nautilus_bybit/common/
enums.rs

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