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 chrono::{DateTime, Datelike, TimeZone, Utc};
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.core.nautilus_pyo3.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.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.core.nautilus_pyo3.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.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.core.nautilus_pyo3.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.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.core.nautilus_pyo3.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.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.core.nautilus_pyo3.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.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.core.nautilus_pyo3.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.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/// Position side as represented in REST/WebSocket payloads.
398#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
399pub enum BybitPositionSide {
400    #[serde(rename = "")]
401    Flat,
402    #[serde(rename = "Buy")]
403    Buy,
404    #[serde(rename = "Sell")]
405    Sell,
406}
407
408/// WebSocket order request operations.
409#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
410pub enum BybitWsOrderRequestOp {
411    #[serde(rename = "order.create")]
412    Create,
413    #[serde(rename = "order.amend")]
414    Amend,
415    #[serde(rename = "order.cancel")]
416    Cancel,
417    #[serde(rename = "order.create-batch")]
418    CreateBatch,
419    #[serde(rename = "order.amend-batch")]
420    AmendBatch,
421    #[serde(rename = "order.cancel-batch")]
422    CancelBatch,
423}
424
425/// Available kline intervals.
426#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
427pub enum BybitKlineInterval {
428    #[serde(rename = "1")]
429    Minute1,
430    #[serde(rename = "3")]
431    Minute3,
432    #[serde(rename = "5")]
433    Minute5,
434    #[serde(rename = "15")]
435    Minute15,
436    #[serde(rename = "30")]
437    Minute30,
438    #[serde(rename = "60")]
439    Hour1,
440    #[serde(rename = "120")]
441    Hour2,
442    #[serde(rename = "240")]
443    Hour4,
444    #[serde(rename = "360")]
445    Hour6,
446    #[serde(rename = "720")]
447    Hour12,
448    #[serde(rename = "D")]
449    Day1,
450    #[serde(rename = "W")]
451    Week1,
452    #[serde(rename = "M")]
453    Month1,
454}
455
456impl BybitKlineInterval {
457    /// Returns the end time in milliseconds for a bar that starts at `start_ms`.
458    ///
459    /// For most intervals this is simply `start_ms + duration`. For monthly bars,
460    /// this calculates the actual first millisecond of the next month to handle
461    /// variable month lengths (28-31 days).
462    #[must_use]
463    pub fn bar_end_time_ms(&self, start_ms: i64) -> i64 {
464        match self {
465            Self::Month1 => {
466                let start_dt = DateTime::from_timestamp_millis(start_ms)
467                    .unwrap_or_else(|| Utc.timestamp_millis_opt(0).unwrap());
468                let (year, month) = if start_dt.month() == 12 {
469                    (start_dt.year() + 1, 1)
470                } else {
471                    (start_dt.year(), start_dt.month() + 1)
472                };
473                Utc.with_ymd_and_hms(year, month, 1, 0, 0, 0)
474                    .single()
475                    .map_or(start_ms + 2_678_400_000, |dt| dt.timestamp_millis())
476            }
477            _ => start_ms + self.duration_ms(),
478        }
479    }
480
481    /// Returns the fixed duration of this interval in milliseconds.
482    ///
483    /// Note: For monthly bars, use [`Self::bar_end_time_ms`] instead as months have
484    /// variable lengths (28-31 days).
485    #[must_use]
486    pub const fn duration_ms(&self) -> i64 {
487        match self {
488            Self::Minute1 => 60_000,
489            Self::Minute3 => 180_000,
490            Self::Minute5 => 300_000,
491            Self::Minute15 => 900_000,
492            Self::Minute30 => 1_800_000,
493            Self::Hour1 => 3_600_000,
494            Self::Hour2 => 7_200_000,
495            Self::Hour4 => 14_400_000,
496            Self::Hour6 => 21_600_000,
497            Self::Hour12 => 43_200_000,
498            Self::Day1 => 86_400_000,
499            Self::Week1 => 604_800_000,
500            Self::Month1 => 2_678_400_000, // 31 days - use bar_end_time_ms() for accurate calculation
501        }
502    }
503}
504
505impl Display for BybitKlineInterval {
506    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
507        let s = match self {
508            Self::Minute1 => "1",
509            Self::Minute3 => "3",
510            Self::Minute5 => "5",
511            Self::Minute15 => "15",
512            Self::Minute30 => "30",
513            Self::Hour1 => "60",
514            Self::Hour2 => "120",
515            Self::Hour4 => "240",
516            Self::Hour6 => "360",
517            Self::Hour12 => "720",
518            Self::Day1 => "D",
519            Self::Week1 => "W",
520            Self::Month1 => "M",
521        };
522        write!(f, "{s}")
523    }
524}
525
526/// Order status values returned by Bybit.
527#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
528#[cfg_attr(
529    feature = "python",
530    pyo3::pyclass(
531        module = "nautilus_trader.core.nautilus_pyo3.bybit",
532        eq,
533        eq_int,
534        from_py_object
535    )
536)]
537#[cfg_attr(
538    feature = "python",
539    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.bybit")
540)]
541pub enum BybitOrderStatus {
542    #[serde(rename = "Created")]
543    Created,
544    #[serde(rename = "New")]
545    New,
546    #[serde(rename = "Rejected")]
547    Rejected,
548    #[serde(rename = "PartiallyFilled")]
549    PartiallyFilled,
550    #[serde(rename = "PartiallyFilledCanceled")]
551    PartiallyFilledCanceled,
552    #[serde(rename = "Filled")]
553    Filled,
554    #[serde(rename = "Cancelled")]
555    Canceled,
556    #[serde(rename = "Untriggered")]
557    Untriggered,
558    #[serde(rename = "Triggered")]
559    Triggered,
560    #[serde(rename = "Deactivated")]
561    Deactivated,
562}
563
564/// Order side enumeration.
565#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
566#[cfg_attr(
567    feature = "python",
568    pyo3::pyclass(
569        module = "nautilus_trader.core.nautilus_pyo3.bybit",
570        eq,
571        eq_int,
572        from_py_object
573    )
574)]
575#[cfg_attr(
576    feature = "python",
577    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.bybit")
578)]
579pub enum BybitOrderSide {
580    #[serde(rename = "")]
581    Unknown,
582    #[serde(rename = "Buy")]
583    Buy,
584    #[serde(rename = "Sell")]
585    Sell,
586}
587
588impl From<BybitOrderSide> for AggressorSide {
589    fn from(value: BybitOrderSide) -> Self {
590        match value {
591            BybitOrderSide::Buy => Self::Buyer,
592            BybitOrderSide::Sell => Self::Seller,
593            BybitOrderSide::Unknown => Self::NoAggressor,
594        }
595    }
596}
597
598impl From<BybitOrderSide> for OrderSide {
599    fn from(value: BybitOrderSide) -> Self {
600        match value {
601            BybitOrderSide::Buy => Self::Buy,
602            BybitOrderSide::Sell => Self::Sell,
603            BybitOrderSide::Unknown => Self::NoOrderSide,
604        }
605    }
606}
607
608impl TryFrom<OrderSide> for BybitOrderSide {
609    type Error = anyhow::Error;
610
611    fn try_from(value: OrderSide) -> Result<Self, Self::Error> {
612        match value {
613            OrderSide::Buy => Ok(Self::Buy),
614            OrderSide::Sell => Ok(Self::Sell),
615            _ => anyhow::bail!("unsupported OrderSide for Bybit: {value:?}"),
616        }
617    }
618}
619
620impl From<BybitTriggerType> for TriggerType {
621    fn from(value: BybitTriggerType) -> Self {
622        match value {
623            BybitTriggerType::None => Self::Default,
624            BybitTriggerType::LastPrice => Self::LastPrice,
625            BybitTriggerType::IndexPrice => Self::IndexPrice,
626            BybitTriggerType::MarkPrice => Self::MarkPrice,
627        }
628    }
629}
630
631impl From<TriggerType> for BybitTriggerType {
632    fn from(value: TriggerType) -> Self {
633        match value {
634            TriggerType::Default | TriggerType::LastPrice | TriggerType::NoTrigger => {
635                Self::LastPrice
636            }
637            TriggerType::IndexPrice => Self::IndexPrice,
638            TriggerType::MarkPrice => Self::MarkPrice,
639            _ => Self::LastPrice,
640        }
641    }
642}
643
644/// Resolves an optional Nautilus trigger type to a Bybit trigger type,
645/// defaulting to `LastPrice` when absent.
646pub fn resolve_trigger_type(trigger_type: Option<TriggerType>) -> BybitTriggerType {
647    trigger_type.map_or(BybitTriggerType::LastPrice, BybitTriggerType::from)
648}
649
650/// Order cancel reason values as returned by Bybit.
651#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
652#[serde(rename_all = "PascalCase")]
653#[cfg_attr(
654    feature = "python",
655    pyo3::pyclass(
656        module = "nautilus_trader.core.nautilus_pyo3.bybit",
657        eq,
658        eq_int,
659        from_py_object
660    )
661)]
662#[cfg_attr(
663    feature = "python",
664    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.bybit")
665)]
666pub enum BybitCancelType {
667    CancelByUser,
668    CancelByReduceOnly,
669    CancelByPrepareLackOfMargin,
670    CancelByPrepareOrderFilter,
671    CancelByPrepareOrderMarginCheckFailed,
672    CancelByPrepareOrderCommission,
673    CancelByPrepareOrderRms,
674    CancelByPrepareOrderOther,
675    CancelByRiskLimit,
676    CancelOnDisconnect,
677    CancelByStopOrdersExceeded,
678    CancelByPzMarketClose,
679    CancelByMarginCheckFailed,
680    CancelByPzTakeover,
681    CancelByAdmin,
682    CancelByTpSlTsClear,
683    CancelByAmendNotModified,
684    CancelByPzCancel,
685    CancelByCrossSelfMatch,
686    CancelBySelfMatchPrevention,
687    #[serde(other)]
688    Other,
689}
690
691/// Order creation origin values.
692#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
693#[serde(rename_all = "PascalCase")]
694pub enum BybitCreateType {
695    CreateByUser,
696    CreateByClosing,
697    CreateByTakeProfit,
698    CreateByStopLoss,
699    CreateByTrailingStop,
700    CreateByStopOrder,
701    CreateByPartialTakeProfit,
702    CreateByPartialStopLoss,
703    CreateByAdl,
704    CreateByLiquidate,
705    CreateByTakeover,
706    CreateByTpsl,
707    #[serde(other)]
708    Other,
709}
710
711/// Venue order type enumeration.
712#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
713#[cfg_attr(
714    feature = "python",
715    pyo3::pyclass(
716        module = "nautilus_trader.core.nautilus_pyo3.bybit",
717        eq,
718        eq_int,
719        from_py_object
720    )
721)]
722#[cfg_attr(
723    feature = "python",
724    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.bybit")
725)]
726pub enum BybitOrderType {
727    #[serde(rename = "Market")]
728    Market,
729    #[serde(rename = "Limit")]
730    Limit,
731    #[serde(rename = "UNKNOWN")]
732    Unknown,
733}
734
735/// Stop order type classification.
736#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
737#[cfg_attr(
738    feature = "python",
739    pyo3::pyclass(
740        module = "nautilus_trader.core.nautilus_pyo3.bybit",
741        eq,
742        eq_int,
743        from_py_object
744    )
745)]
746#[cfg_attr(
747    feature = "python",
748    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.bybit")
749)]
750pub enum BybitStopOrderType {
751    #[serde(rename = "")]
752    None,
753    #[serde(rename = "UNKNOWN")]
754    Unknown,
755    #[serde(rename = "TakeProfit")]
756    TakeProfit,
757    #[serde(rename = "StopLoss")]
758    StopLoss,
759    #[serde(rename = "TrailingStop")]
760    TrailingStop,
761    #[serde(rename = "Stop")]
762    Stop,
763    #[serde(rename = "PartialTakeProfit")]
764    PartialTakeProfit,
765    #[serde(rename = "PartialStopLoss")]
766    PartialStopLoss,
767    #[serde(rename = "tpslOrder")]
768    TpslOrder,
769    #[serde(rename = "OcoOrder")]
770    OcoOrder,
771    #[serde(rename = "MmRateClose")]
772    MmRateClose,
773    #[serde(rename = "BidirectionalTpslOrder")]
774    BidirectionalTpslOrder,
775}
776
777/// Trigger type configuration.
778#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
779#[cfg_attr(
780    feature = "python",
781    pyo3::pyclass(
782        module = "nautilus_trader.core.nautilus_pyo3.bybit",
783        eq,
784        eq_int,
785        from_py_object
786    )
787)]
788#[cfg_attr(
789    feature = "python",
790    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.bybit")
791)]
792pub enum BybitTriggerType {
793    #[serde(rename = "")]
794    None,
795    #[serde(rename = "LastPrice")]
796    LastPrice,
797    #[serde(rename = "IndexPrice")]
798    IndexPrice,
799    #[serde(rename = "MarkPrice")]
800    MarkPrice,
801}
802
803/// Trigger direction integers used by the API.
804#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize_repr, Deserialize_repr)]
805#[repr(i32)]
806#[cfg_attr(
807    feature = "python",
808    pyo3::pyclass(
809        module = "nautilus_trader.core.nautilus_pyo3.bybit",
810        eq,
811        eq_int,
812        from_py_object
813    )
814)]
815#[cfg_attr(
816    feature = "python",
817    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.bybit")
818)]
819pub enum BybitTriggerDirection {
820    None = 0,
821    RisesTo = 1,
822    FallsTo = 2,
823}
824
825/// Take-profit/stop-loss mode for derivatives orders.
826#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
827#[serde(rename_all = "PascalCase")]
828#[cfg_attr(
829    feature = "python",
830    pyo3::pyclass(
831        module = "nautilus_trader.core.nautilus_pyo3.bybit",
832        eq,
833        eq_int,
834        from_py_object
835    )
836)]
837#[cfg_attr(
838    feature = "python",
839    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.bybit")
840)]
841pub enum BybitTpSlMode {
842    Full,
843    Partial,
844    #[serde(other)]
845    Unknown,
846}
847
848/// Time-in-force enumeration.
849#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
850#[cfg_attr(
851    feature = "python",
852    pyo3::pyclass(
853        module = "nautilus_trader.core.nautilus_pyo3.bybit",
854        eq,
855        eq_int,
856        from_py_object
857    )
858)]
859#[cfg_attr(
860    feature = "python",
861    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.bybit")
862)]
863pub enum BybitTimeInForce {
864    #[serde(rename = "GTC")]
865    Gtc,
866    #[serde(rename = "IOC")]
867    Ioc,
868    #[serde(rename = "FOK")]
869    Fok,
870    #[serde(rename = "PostOnly")]
871    PostOnly,
872}
873
874/// Execution type values used in execution reports.
875///
876/// Reference: <https://bybit-exchange.github.io/docs/v5/enum#exectype>.
877#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
878pub enum BybitExecType {
879    #[serde(rename = "Trade")]
880    Trade,
881    #[serde(rename = "AdlTrade")]
882    AdlTrade,
883    #[serde(rename = "Funding")]
884    Funding,
885    #[serde(rename = "BustTrade")]
886    BustTrade,
887    #[serde(rename = "Delivery")]
888    Delivery,
889    #[serde(rename = "Settle")]
890    Settle,
891    #[serde(rename = "BlockTrade")]
892    BlockTrade,
893    #[serde(rename = "MovePosition")]
894    MovePosition,
895    #[serde(rename = "UNKNOWN")]
896    Unknown,
897}
898
899impl BybitExecType {
900    /// Returns `true` if this execution was generated by the venue rather than the user.
901    ///
902    /// This covers auto-deleveraging (`AdlTrade`), liquidation takeovers (`BustTrade`),
903    /// scheduled deliveries (`Delivery`), and settlement (`Settle`).
904    #[must_use]
905    pub const fn is_exchange_generated(&self) -> bool {
906        matches!(
907            self,
908            Self::AdlTrade | Self::BustTrade | Self::Delivery | Self::Settle
909        )
910    }
911}
912
913/// Transaction types for wallet funding records.
914#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
915pub enum BybitTransactionType {
916    #[serde(rename = "TRANSFER_IN")]
917    TransferIn,
918    #[serde(rename = "TRANSFER_OUT")]
919    TransferOut,
920    #[serde(rename = "TRADE")]
921    Trade,
922    #[serde(rename = "SETTLEMENT")]
923    Settlement,
924    #[serde(rename = "DELIVERY")]
925    Delivery,
926    #[serde(rename = "LIQUIDATION")]
927    Liquidation,
928    #[serde(rename = "AIRDRP")]
929    Airdrop,
930}
931
932/// Endpoint classifications used by the Bybit API.
933#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
934#[serde(rename_all = "UPPERCASE")]
935pub enum BybitEndpointType {
936    None,
937    Asset,
938    Market,
939    Account,
940    Trade,
941    Position,
942    User,
943}
944
945/// Filter for open orders query.
946///
947/// Used with `GET /v5/order/realtime` to filter order status.
948#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, Serialize_repr, Deserialize_repr)]
949#[repr(i32)]
950#[cfg_attr(
951    feature = "python",
952    pyo3::pyclass(
953        module = "nautilus_trader.core.nautilus_pyo3.bybit",
954        eq,
955        eq_int,
956        from_py_object
957    )
958)]
959#[cfg_attr(
960    feature = "python",
961    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.bybit")
962)]
963pub enum BybitOpenOnly {
964    /// Query open status orders only (New, PartiallyFilled).
965    #[default]
966    OpenOnly = 0,
967    /// Query up to 500 recent closed orders (cancelled, rejected, filled).
968    ClosedRecent = 1,
969}
970
971/// Order filter for querying specific order types.
972///
973/// Used with `GET /v5/order/realtime` to filter by order category.
974#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, Serialize, Deserialize)]
975#[cfg_attr(
976    feature = "python",
977    pyo3::pyclass(
978        module = "nautilus_trader.core.nautilus_pyo3.bybit",
979        eq,
980        eq_int,
981        from_py_object
982    )
983)]
984#[cfg_attr(
985    feature = "python",
986    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.bybit")
987)]
988pub enum BybitOrderFilter {
989    /// Active orders (default).
990    #[default]
991    Order,
992    /// Conditional orders (futures and spot).
993    StopOrder,
994    /// Spot take-profit/stop-loss orders.
995    #[serde(rename = "tpslOrder")]
996    TpslOrder,
997    /// Spot one-cancels-other orders.
998    OcoOrder,
999    /// Spot bidirectional TP/SL orders.
1000    BidirectionalTpslOrder,
1001}
1002
1003/// Margin actions for spot margin trading operations.
1004#[derive(
1005    Clone,
1006    Copy,
1007    Debug,
1008    strum::Display,
1009    Eq,
1010    PartialEq,
1011    Hash,
1012    AsRefStr,
1013    EnumIter,
1014    EnumString,
1015    Serialize,
1016    Deserialize,
1017)]
1018#[serde(rename_all = "snake_case")]
1019#[strum(serialize_all = "snake_case")]
1020#[cfg_attr(
1021    feature = "python",
1022    pyo3::pyclass(
1023        eq,
1024        eq_int,
1025        hash,
1026        frozen,
1027        rename_all = "SCREAMING_SNAKE_CASE",
1028        module = "nautilus_trader.core.nautilus_pyo3.bybit",
1029        from_py_object,
1030    )
1031)]
1032#[cfg_attr(
1033    feature = "python",
1034    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.bybit")
1035)]
1036pub enum BybitMarginAction {
1037    /// Borrow funds for margin trading.
1038    Borrow,
1039    /// Repay borrowed funds.
1040    Repay,
1041    /// Query current borrowed amount.
1042    GetBorrowAmount,
1043}
1044
1045/// Position status enumeration.
1046#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
1047#[serde(rename_all = "PascalCase")]
1048pub enum BybitPositionStatus {
1049    Normal,
1050    Settle,
1051    Delivering,
1052    #[serde(other)]
1053    Other,
1054}
1055
1056/// Market unit for spot market orders.
1057#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
1058pub enum BybitMarketUnit {
1059    #[serde(rename = "baseCoin")]
1060    BaseCoin,
1061    #[serde(rename = "quoteCoin")]
1062    QuoteCoin,
1063}
1064
1065/// Self-match prevention type.
1066#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
1067pub enum BybitSmpType {
1068    None,
1069    CancelMaker,
1070    CancelTaker,
1071    CancelBoth,
1072    #[serde(other)]
1073    Other,
1074}
1075
1076#[cfg(test)]
1077mod tests {
1078    use rstest::rstest;
1079
1080    use super::*;
1081
1082    #[rstest]
1083    #[case::minute1(BybitKlineInterval::Minute1, 60_000)]
1084    #[case::minute3(BybitKlineInterval::Minute3, 180_000)]
1085    #[case::minute5(BybitKlineInterval::Minute5, 300_000)]
1086    #[case::minute15(BybitKlineInterval::Minute15, 900_000)]
1087    #[case::minute30(BybitKlineInterval::Minute30, 1_800_000)]
1088    #[case::hour1(BybitKlineInterval::Hour1, 3_600_000)]
1089    #[case::hour2(BybitKlineInterval::Hour2, 7_200_000)]
1090    #[case::hour4(BybitKlineInterval::Hour4, 14_400_000)]
1091    #[case::hour6(BybitKlineInterval::Hour6, 21_600_000)]
1092    #[case::hour12(BybitKlineInterval::Hour12, 43_200_000)]
1093    #[case::day1(BybitKlineInterval::Day1, 86_400_000)]
1094    #[case::week1(BybitKlineInterval::Week1, 604_800_000)]
1095    #[case::month1(BybitKlineInterval::Month1, 2_678_400_000)]
1096    fn test_kline_interval_duration_ms(
1097        #[case] interval: BybitKlineInterval,
1098        #[case] expected_ms: i64,
1099    ) {
1100        assert_eq!(interval.duration_ms(), expected_ms);
1101    }
1102
1103    #[rstest]
1104    fn test_bar_end_time_ms_non_monthly_adds_duration() {
1105        let interval = BybitKlineInterval::Minute1;
1106        let start_ms = 1704067200000i64;
1107        assert_eq!(interval.bar_end_time_ms(start_ms), start_ms + 60_000);
1108    }
1109
1110    #[rstest]
1111    #[case::jan_31_days(1704067200000i64, 1706745600000i64)]
1112    #[case::feb_leap_year_29_days(1706745600000i64, 1709251200000i64)]
1113    #[case::apr_30_days(1711929600000i64, 1714521600000i64)]
1114    #[case::dec_to_next_year(1733011200000i64, 1735689600000i64)]
1115    fn test_bar_end_time_ms_monthly_variable_lengths(
1116        #[case] start_ms: i64,
1117        #[case] expected_end_ms: i64,
1118    ) {
1119        let interval = BybitKlineInterval::Month1;
1120        assert_eq!(interval.bar_end_time_ms(start_ms), expected_end_ms);
1121    }
1122
1123    #[rstest]
1124    #[case(BybitExecType::Trade, false)]
1125    #[case(BybitExecType::AdlTrade, true)]
1126    #[case(BybitExecType::BustTrade, true)]
1127    #[case(BybitExecType::Delivery, true)]
1128    #[case(BybitExecType::Settle, true)]
1129    #[case(BybitExecType::Funding, false)]
1130    fn test_exec_type_is_exchange_generated(
1131        #[case] exec_type: BybitExecType,
1132        #[case] expected: bool,
1133    ) {
1134        assert_eq!(exec_type.is_exchange_generated(), expected);
1135    }
1136}