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