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)]
69pub enum BybitMarginMode {
70    IsolatedMargin,
71    RegularMargin,
72    PortfolioMargin,
73}
74
75/// Position mode as returned by the v5 API.
76#[derive(
77    Clone,
78    Copy,
79    Debug,
80    strum::Display,
81    Eq,
82    PartialEq,
83    Hash,
84    AsRefStr,
85    EnumIter,
86    EnumString,
87    Serialize_repr,
88    Deserialize_repr,
89)]
90#[repr(i32)]
91#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
92#[cfg_attr(
93    feature = "python",
94    pyo3::pyclass(
95        eq,
96        eq_int,
97        rename_all = "SCREAMING_SNAKE_CASE",
98        module = "nautilus_trader.core.nautilus_pyo3.bybit",
99        from_py_object
100    )
101)]
102pub enum BybitPositionMode {
103    /// Merged single position mode.
104    MergedSingle = 0,
105    /// Dual-side hedged position mode.
106    BothSides = 3,
107}
108
109/// Position index values used for hedge mode payloads.
110#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize_repr, Deserialize_repr)]
111#[repr(i32)]
112pub enum BybitPositionIdx {
113    /// One-way mode position identifier.
114    OneWay = 0,
115    /// Buy side of a hedge-mode position.
116    BuyHedge = 1,
117    /// Sell side of a hedge-mode position.
118    SellHedge = 2,
119}
120
121/// Account type enumeration.
122#[derive(
123    Copy,
124    Clone,
125    Debug,
126    strum::Display,
127    PartialEq,
128    Eq,
129    Hash,
130    AsRefStr,
131    EnumIter,
132    EnumString,
133    Serialize,
134    Deserialize,
135)]
136#[serde(rename_all = "UPPERCASE")]
137#[cfg_attr(
138    feature = "python",
139    pyo3::pyclass(
140        eq,
141        eq_int,
142        rename_all = "SCREAMING_SNAKE_CASE",
143        module = "nautilus_trader.core.nautilus_pyo3.bybit",
144        from_py_object
145    )
146)]
147pub enum BybitAccountType {
148    Unified,
149}
150
151/// Environments supported by the Bybit API stack.
152#[derive(
153    Copy,
154    Clone,
155    Debug,
156    strum::Display,
157    PartialEq,
158    Eq,
159    Hash,
160    AsRefStr,
161    EnumIter,
162    EnumString,
163    Serialize,
164    Deserialize,
165)]
166#[serde(rename_all = "lowercase")]
167#[cfg_attr(
168    feature = "python",
169    pyo3::pyclass(
170        eq,
171        eq_int,
172        rename_all = "SCREAMING_SNAKE_CASE",
173        module = "nautilus_trader.core.nautilus_pyo3.bybit",
174        from_py_object
175    )
176)]
177pub enum BybitEnvironment {
178    /// Live trading environment.
179    Mainnet,
180    /// Demo (paper trading) environment.
181    Demo,
182    /// Testnet environment for spot/derivatives.
183    Testnet,
184}
185
186/// Product categories supported by the v5 API.
187#[derive(
188    Copy,
189    Clone,
190    Debug,
191    strum::Display,
192    Default,
193    PartialEq,
194    Eq,
195    Hash,
196    AsRefStr,
197    EnumIter,
198    EnumString,
199    Serialize,
200    Deserialize,
201)]
202#[serde(rename_all = "lowercase")]
203#[cfg_attr(
204    feature = "python",
205    pyo3::pyclass(
206        eq,
207        eq_int,
208        rename_all = "SCREAMING_SNAKE_CASE",
209        module = "nautilus_trader.core.nautilus_pyo3.bybit",
210        from_py_object
211    )
212)]
213pub enum BybitProductType {
214    #[default]
215    Spot,
216    Linear,
217    Inverse,
218    Option,
219}
220
221/// Spot margin trading enablement states.
222#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
223pub enum BybitMarginTrading {
224    #[serde(rename = "none")]
225    None,
226    #[serde(rename = "utaOnly")]
227    UtaOnly,
228    #[serde(rename = "both")]
229    Both,
230    #[serde(other)]
231    Other,
232}
233
234/// Innovation market flag for spot instruments.
235#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
236pub enum BybitInnovationFlag {
237    #[serde(rename = "0")]
238    Standard,
239    #[serde(rename = "1")]
240    Innovation,
241    #[serde(other)]
242    Other,
243}
244
245/// Instrument lifecycle status values.
246#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
247#[serde(rename_all = "PascalCase")]
248pub enum BybitInstrumentStatus {
249    Trading,
250    Settled,
251    Delivering,
252    ListedOnly,
253    PendingListing,
254    PreTrading,
255    Closed,
256    Suspended,
257    #[serde(other)]
258    Other,
259}
260
261impl BybitProductType {
262    /// Returns the canonical lowercase identifier used for REST/WS routes.
263    #[must_use]
264    pub const fn as_str(self) -> &'static str {
265        match self {
266            Self::Spot => "spot",
267            Self::Linear => "linear",
268            Self::Inverse => "inverse",
269            Self::Option => "option",
270        }
271    }
272
273    /// Returns the uppercase suffix used in instrument identifiers (e.g. `-LINEAR`).
274    #[must_use]
275    pub const fn suffix(self) -> &'static str {
276        match self {
277            Self::Spot => "-SPOT",
278            Self::Linear => "-LINEAR",
279            Self::Inverse => "-INVERSE",
280            Self::Option => "-OPTION",
281        }
282    }
283
284    /// Returns `true` if the product is a spot instrument.
285    #[must_use]
286    pub fn is_spot(self) -> bool {
287        matches!(self, Self::Spot)
288    }
289
290    /// Returns `true` if the product is a linear contract.
291    #[must_use]
292    pub fn is_linear(self) -> bool {
293        matches!(self, Self::Linear)
294    }
295
296    /// Returns `true` if the product is an inverse contract.
297    #[must_use]
298    pub fn is_inverse(self) -> bool {
299        matches!(self, Self::Inverse)
300    }
301
302    /// Returns `true` if the product is an option contract.
303    #[must_use]
304    pub fn is_option(self) -> bool {
305        matches!(self, Self::Option)
306    }
307}
308
309/// Contract type enumeration for linear and inverse derivatives.
310#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
311#[serde(rename_all = "PascalCase")]
312pub enum BybitContractType {
313    LinearPerpetual,
314    LinearFutures,
315    InversePerpetual,
316    InverseFutures,
317}
318
319/// Option flavour values.
320#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
321#[serde(rename_all = "PascalCase")]
322pub enum BybitOptionType {
323    Call,
324    Put,
325}
326
327/// Position side as represented in REST/WebSocket payloads.
328#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
329pub enum BybitPositionSide {
330    #[serde(rename = "")]
331    Flat,
332    #[serde(rename = "Buy")]
333    Buy,
334    #[serde(rename = "Sell")]
335    Sell,
336}
337
338/// WebSocket order request operations.
339#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
340pub enum BybitWsOrderRequestOp {
341    #[serde(rename = "order.create")]
342    Create,
343    #[serde(rename = "order.amend")]
344    Amend,
345    #[serde(rename = "order.cancel")]
346    Cancel,
347    #[serde(rename = "order.create-batch")]
348    CreateBatch,
349    #[serde(rename = "order.amend-batch")]
350    AmendBatch,
351    #[serde(rename = "order.cancel-batch")]
352    CancelBatch,
353}
354
355/// Available kline intervals.
356#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
357pub enum BybitKlineInterval {
358    #[serde(rename = "1")]
359    Minute1,
360    #[serde(rename = "3")]
361    Minute3,
362    #[serde(rename = "5")]
363    Minute5,
364    #[serde(rename = "15")]
365    Minute15,
366    #[serde(rename = "30")]
367    Minute30,
368    #[serde(rename = "60")]
369    Hour1,
370    #[serde(rename = "120")]
371    Hour2,
372    #[serde(rename = "240")]
373    Hour4,
374    #[serde(rename = "360")]
375    Hour6,
376    #[serde(rename = "720")]
377    Hour12,
378    #[serde(rename = "D")]
379    Day1,
380    #[serde(rename = "W")]
381    Week1,
382    #[serde(rename = "M")]
383    Month1,
384}
385
386impl BybitKlineInterval {
387    /// Returns the end time in milliseconds for a bar that starts at `start_ms`.
388    ///
389    /// For most intervals this is simply `start_ms + duration`. For monthly bars,
390    /// this calculates the actual first millisecond of the next month to handle
391    /// variable month lengths (28-31 days).
392    #[must_use]
393    pub fn bar_end_time_ms(&self, start_ms: i64) -> i64 {
394        match self {
395            Self::Month1 => {
396                let start_dt = DateTime::from_timestamp_millis(start_ms)
397                    .unwrap_or_else(|| Utc.timestamp_millis_opt(0).unwrap());
398                let (year, month) = if start_dt.month() == 12 {
399                    (start_dt.year() + 1, 1)
400                } else {
401                    (start_dt.year(), start_dt.month() + 1)
402                };
403                Utc.with_ymd_and_hms(year, month, 1, 0, 0, 0)
404                    .single()
405                    .map_or(start_ms + 2_678_400_000, |dt| dt.timestamp_millis())
406            }
407            _ => start_ms + self.duration_ms(),
408        }
409    }
410
411    /// Returns the fixed duration of this interval in milliseconds.
412    ///
413    /// Note: For monthly bars, use [`Self::bar_end_time_ms`] instead as months have
414    /// variable lengths (28-31 days).
415    #[must_use]
416    pub const fn duration_ms(&self) -> i64 {
417        match self {
418            Self::Minute1 => 60_000,
419            Self::Minute3 => 180_000,
420            Self::Minute5 => 300_000,
421            Self::Minute15 => 900_000,
422            Self::Minute30 => 1_800_000,
423            Self::Hour1 => 3_600_000,
424            Self::Hour2 => 7_200_000,
425            Self::Hour4 => 14_400_000,
426            Self::Hour6 => 21_600_000,
427            Self::Hour12 => 43_200_000,
428            Self::Day1 => 86_400_000,
429            Self::Week1 => 604_800_000,
430            Self::Month1 => 2_678_400_000, // 31 days - use bar_end_time_ms() for accurate calculation
431        }
432    }
433}
434
435impl Display for BybitKlineInterval {
436    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
437        let s = match self {
438            Self::Minute1 => "1",
439            Self::Minute3 => "3",
440            Self::Minute5 => "5",
441            Self::Minute15 => "15",
442            Self::Minute30 => "30",
443            Self::Hour1 => "60",
444            Self::Hour2 => "120",
445            Self::Hour4 => "240",
446            Self::Hour6 => "360",
447            Self::Hour12 => "720",
448            Self::Day1 => "D",
449            Self::Week1 => "W",
450            Self::Month1 => "M",
451        };
452        write!(f, "{s}")
453    }
454}
455
456/// Order status values returned by Bybit.
457#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
458#[cfg_attr(
459    feature = "python",
460    pyo3::pyclass(
461        module = "nautilus_trader.core.nautilus_pyo3.bybit",
462        eq,
463        eq_int,
464        from_py_object
465    )
466)]
467pub enum BybitOrderStatus {
468    #[serde(rename = "Created")]
469    Created,
470    #[serde(rename = "New")]
471    New,
472    #[serde(rename = "Rejected")]
473    Rejected,
474    #[serde(rename = "PartiallyFilled")]
475    PartiallyFilled,
476    #[serde(rename = "PartiallyFilledCanceled")]
477    PartiallyFilledCanceled,
478    #[serde(rename = "Filled")]
479    Filled,
480    #[serde(rename = "Cancelled")]
481    Canceled,
482    #[serde(rename = "Untriggered")]
483    Untriggered,
484    #[serde(rename = "Triggered")]
485    Triggered,
486    #[serde(rename = "Deactivated")]
487    Deactivated,
488}
489
490/// Order side enumeration.
491#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
492#[cfg_attr(
493    feature = "python",
494    pyo3::pyclass(
495        module = "nautilus_trader.core.nautilus_pyo3.bybit",
496        eq,
497        eq_int,
498        from_py_object
499    )
500)]
501pub enum BybitOrderSide {
502    #[serde(rename = "")]
503    Unknown,
504    #[serde(rename = "Buy")]
505    Buy,
506    #[serde(rename = "Sell")]
507    Sell,
508}
509
510impl From<BybitOrderSide> for AggressorSide {
511    fn from(value: BybitOrderSide) -> Self {
512        match value {
513            BybitOrderSide::Buy => Self::Buyer,
514            BybitOrderSide::Sell => Self::Seller,
515            BybitOrderSide::Unknown => Self::NoAggressor,
516        }
517    }
518}
519
520impl From<BybitOrderSide> for OrderSide {
521    fn from(value: BybitOrderSide) -> Self {
522        match value {
523            BybitOrderSide::Buy => Self::Buy,
524            BybitOrderSide::Sell => Self::Sell,
525            BybitOrderSide::Unknown => Self::NoOrderSide,
526        }
527    }
528}
529
530impl TryFrom<OrderSide> for BybitOrderSide {
531    type Error = anyhow::Error;
532
533    fn try_from(value: OrderSide) -> Result<Self, Self::Error> {
534        match value {
535            OrderSide::Buy => Ok(Self::Buy),
536            OrderSide::Sell => Ok(Self::Sell),
537            _ => anyhow::bail!("unsupported OrderSide for Bybit: {value:?}"),
538        }
539    }
540}
541
542impl From<BybitTriggerType> for TriggerType {
543    fn from(value: BybitTriggerType) -> Self {
544        match value {
545            BybitTriggerType::None => Self::Default,
546            BybitTriggerType::LastPrice => Self::LastPrice,
547            BybitTriggerType::IndexPrice => Self::IndexPrice,
548            BybitTriggerType::MarkPrice => Self::MarkPrice,
549        }
550    }
551}
552
553/// Order cancel reason values as returned by Bybit.
554#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
555#[serde(rename_all = "PascalCase")]
556#[cfg_attr(
557    feature = "python",
558    pyo3::pyclass(
559        module = "nautilus_trader.core.nautilus_pyo3.bybit",
560        eq,
561        eq_int,
562        from_py_object
563    )
564)]
565pub enum BybitCancelType {
566    CancelByUser,
567    CancelByReduceOnly,
568    CancelByPrepareLackOfMargin,
569    CancelByPrepareOrderFilter,
570    CancelByPrepareOrderMarginCheckFailed,
571    CancelByPrepareOrderCommission,
572    CancelByPrepareOrderRms,
573    CancelByPrepareOrderOther,
574    CancelByRiskLimit,
575    CancelOnDisconnect,
576    CancelByStopOrdersExceeded,
577    CancelByPzMarketClose,
578    CancelByMarginCheckFailed,
579    CancelByPzTakeover,
580    CancelByAdmin,
581    CancelByTpSlTsClear,
582    CancelByAmendNotModified,
583    CancelByPzCancel,
584    CancelByCrossSelfMatch,
585    CancelBySelfMatchPrevention,
586    #[serde(other)]
587    Other,
588}
589
590/// Order creation origin values.
591#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
592#[serde(rename_all = "PascalCase")]
593pub enum BybitCreateType {
594    CreateByUser,
595    CreateByClosing,
596    CreateByTakeProfit,
597    CreateByStopLoss,
598    CreateByTrailingStop,
599    CreateByStopOrder,
600    CreateByPartialTakeProfit,
601    CreateByPartialStopLoss,
602    CreateByAdl,
603    CreateByLiquidate,
604    CreateByTakeover,
605    CreateByTpsl,
606    #[serde(other)]
607    Other,
608}
609
610/// Venue order type enumeration.
611#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
612#[cfg_attr(
613    feature = "python",
614    pyo3::pyclass(
615        module = "nautilus_trader.core.nautilus_pyo3.bybit",
616        eq,
617        eq_int,
618        from_py_object
619    )
620)]
621pub enum BybitOrderType {
622    #[serde(rename = "Market")]
623    Market,
624    #[serde(rename = "Limit")]
625    Limit,
626    #[serde(rename = "UNKNOWN")]
627    Unknown,
628}
629
630/// Stop order type classification.
631#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
632#[cfg_attr(
633    feature = "python",
634    pyo3::pyclass(
635        module = "nautilus_trader.core.nautilus_pyo3.bybit",
636        eq,
637        eq_int,
638        from_py_object
639    )
640)]
641pub enum BybitStopOrderType {
642    #[serde(rename = "")]
643    None,
644    #[serde(rename = "UNKNOWN")]
645    Unknown,
646    #[serde(rename = "TakeProfit")]
647    TakeProfit,
648    #[serde(rename = "StopLoss")]
649    StopLoss,
650    #[serde(rename = "TrailingStop")]
651    TrailingStop,
652    #[serde(rename = "Stop")]
653    Stop,
654    #[serde(rename = "PartialTakeProfit")]
655    PartialTakeProfit,
656    #[serde(rename = "PartialStopLoss")]
657    PartialStopLoss,
658    #[serde(rename = "tpslOrder")]
659    TpslOrder,
660    #[serde(rename = "OcoOrder")]
661    OcoOrder,
662    #[serde(rename = "MmRateClose")]
663    MmRateClose,
664    #[serde(rename = "BidirectionalTpslOrder")]
665    BidirectionalTpslOrder,
666}
667
668/// Trigger type configuration.
669#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
670#[cfg_attr(
671    feature = "python",
672    pyo3::pyclass(
673        module = "nautilus_trader.core.nautilus_pyo3.bybit",
674        eq,
675        eq_int,
676        from_py_object
677    )
678)]
679pub enum BybitTriggerType {
680    #[serde(rename = "")]
681    None,
682    #[serde(rename = "LastPrice")]
683    LastPrice,
684    #[serde(rename = "IndexPrice")]
685    IndexPrice,
686    #[serde(rename = "MarkPrice")]
687    MarkPrice,
688}
689
690/// Trigger direction integers used by the API.
691#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize_repr, Deserialize_repr)]
692#[repr(i32)]
693#[cfg_attr(
694    feature = "python",
695    pyo3::pyclass(
696        module = "nautilus_trader.core.nautilus_pyo3.bybit",
697        eq,
698        eq_int,
699        from_py_object
700    )
701)]
702pub enum BybitTriggerDirection {
703    None = 0,
704    RisesTo = 1,
705    FallsTo = 2,
706}
707
708/// Take-profit/stop-loss mode for derivatives orders.
709#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
710#[serde(rename_all = "PascalCase")]
711#[cfg_attr(
712    feature = "python",
713    pyo3::pyclass(
714        module = "nautilus_trader.core.nautilus_pyo3.bybit",
715        eq,
716        eq_int,
717        from_py_object
718    )
719)]
720pub enum BybitTpSlMode {
721    Full,
722    Partial,
723    #[serde(other)]
724    Unknown,
725}
726
727/// Time-in-force enumeration.
728#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
729#[cfg_attr(
730    feature = "python",
731    pyo3::pyclass(
732        module = "nautilus_trader.core.nautilus_pyo3.bybit",
733        eq,
734        eq_int,
735        from_py_object
736    )
737)]
738pub enum BybitTimeInForce {
739    #[serde(rename = "GTC")]
740    Gtc,
741    #[serde(rename = "IOC")]
742    Ioc,
743    #[serde(rename = "FOK")]
744    Fok,
745    #[serde(rename = "PostOnly")]
746    PostOnly,
747}
748
749/// Execution type values used in execution reports.
750#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
751pub enum BybitExecType {
752    #[serde(rename = "Trade")]
753    Trade,
754    #[serde(rename = "AdlTrade")]
755    AdlTrade,
756    #[serde(rename = "Funding")]
757    Funding,
758    #[serde(rename = "BustTrade")]
759    BustTrade,
760    #[serde(rename = "Delivery")]
761    Delivery,
762    #[serde(rename = "Settle")]
763    Settle,
764    #[serde(rename = "BlockTrade")]
765    BlockTrade,
766    #[serde(rename = "MovePosition")]
767    MovePosition,
768    #[serde(rename = "UNKNOWN")]
769    Unknown,
770}
771
772/// Transaction types for wallet funding records.
773#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
774pub enum BybitTransactionType {
775    #[serde(rename = "TRANSFER_IN")]
776    TransferIn,
777    #[serde(rename = "TRANSFER_OUT")]
778    TransferOut,
779    #[serde(rename = "TRADE")]
780    Trade,
781    #[serde(rename = "SETTLEMENT")]
782    Settlement,
783    #[serde(rename = "DELIVERY")]
784    Delivery,
785    #[serde(rename = "LIQUIDATION")]
786    Liquidation,
787    #[serde(rename = "AIRDRP")]
788    Airdrop,
789}
790
791/// Endpoint classifications used by the Bybit API.
792#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
793#[serde(rename_all = "UPPERCASE")]
794pub enum BybitEndpointType {
795    None,
796    Asset,
797    Market,
798    Account,
799    Trade,
800    Position,
801    User,
802}
803
804/// Filter for open orders query.
805///
806/// Used with `GET /v5/order/realtime` to filter order status.
807#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, Serialize_repr, Deserialize_repr)]
808#[repr(i32)]
809#[cfg_attr(
810    feature = "python",
811    pyo3::pyclass(
812        module = "nautilus_trader.core.nautilus_pyo3.bybit",
813        eq,
814        eq_int,
815        from_py_object
816    )
817)]
818pub enum BybitOpenOnly {
819    /// Query open status orders only (New, PartiallyFilled).
820    #[default]
821    OpenOnly = 0,
822    /// Query up to 500 recent closed orders (cancelled, rejected, filled).
823    ClosedRecent = 1,
824}
825
826/// Order filter for querying specific order types.
827///
828/// Used with `GET /v5/order/realtime` to filter by order category.
829#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, Serialize, Deserialize)]
830#[cfg_attr(
831    feature = "python",
832    pyo3::pyclass(
833        module = "nautilus_trader.core.nautilus_pyo3.bybit",
834        eq,
835        eq_int,
836        from_py_object
837    )
838)]
839pub enum BybitOrderFilter {
840    /// Active orders (default).
841    #[default]
842    Order,
843    /// Conditional orders (futures and spot).
844    StopOrder,
845    /// Spot take-profit/stop-loss orders.
846    #[serde(rename = "tpslOrder")]
847    TpslOrder,
848    /// Spot one-cancels-other orders.
849    OcoOrder,
850    /// Spot bidirectional TP/SL orders.
851    BidirectionalTpslOrder,
852}
853
854/// Margin actions for spot margin trading operations.
855#[derive(
856    Clone,
857    Copy,
858    Debug,
859    strum::Display,
860    Eq,
861    PartialEq,
862    Hash,
863    AsRefStr,
864    EnumIter,
865    EnumString,
866    Serialize,
867    Deserialize,
868)]
869#[serde(rename_all = "snake_case")]
870#[strum(serialize_all = "snake_case")]
871#[cfg_attr(
872    feature = "python",
873    pyo3::pyclass(
874        eq,
875        eq_int,
876        hash,
877        frozen,
878        rename_all = "SCREAMING_SNAKE_CASE",
879        module = "nautilus_trader.core.nautilus_pyo3.bybit",
880        from_py_object,
881    )
882)]
883pub enum BybitMarginAction {
884    /// Borrow funds for margin trading.
885    Borrow,
886    /// Repay borrowed funds.
887    Repay,
888    /// Query current borrowed amount.
889    GetBorrowAmount,
890}
891
892#[cfg(test)]
893mod tests {
894    use rstest::rstest;
895
896    use super::*;
897
898    #[rstest]
899    #[case::minute1(BybitKlineInterval::Minute1, 60_000)]
900    #[case::minute3(BybitKlineInterval::Minute3, 180_000)]
901    #[case::minute5(BybitKlineInterval::Minute5, 300_000)]
902    #[case::minute15(BybitKlineInterval::Minute15, 900_000)]
903    #[case::minute30(BybitKlineInterval::Minute30, 1_800_000)]
904    #[case::hour1(BybitKlineInterval::Hour1, 3_600_000)]
905    #[case::hour2(BybitKlineInterval::Hour2, 7_200_000)]
906    #[case::hour4(BybitKlineInterval::Hour4, 14_400_000)]
907    #[case::hour6(BybitKlineInterval::Hour6, 21_600_000)]
908    #[case::hour12(BybitKlineInterval::Hour12, 43_200_000)]
909    #[case::day1(BybitKlineInterval::Day1, 86_400_000)]
910    #[case::week1(BybitKlineInterval::Week1, 604_800_000)]
911    #[case::month1(BybitKlineInterval::Month1, 2_678_400_000)]
912    fn test_kline_interval_duration_ms(
913        #[case] interval: BybitKlineInterval,
914        #[case] expected_ms: i64,
915    ) {
916        assert_eq!(interval.duration_ms(), expected_ms);
917    }
918
919    #[rstest]
920    fn test_bar_end_time_ms_non_monthly_adds_duration() {
921        let interval = BybitKlineInterval::Minute1;
922        let start_ms = 1704067200000i64;
923        assert_eq!(interval.bar_end_time_ms(start_ms), start_ms + 60_000);
924    }
925
926    #[rstest]
927    #[case::jan_31_days(1704067200000i64, 1706745600000i64)]
928    #[case::feb_leap_year_29_days(1706745600000i64, 1709251200000i64)]
929    #[case::apr_30_days(1711929600000i64, 1714521600000i64)]
930    #[case::dec_to_next_year(1733011200000i64, 1735689600000i64)]
931    fn test_bar_end_time_ms_monthly_variable_lengths(
932        #[case] start_ms: i64,
933        #[case] expected_end_ms: i64,
934    ) {
935        let interval = BybitKlineInterval::Month1;
936        assert_eq!(interval.bar_end_time_ms(start_ms), expected_end_ms);
937    }
938}