1use 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#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize_repr, Deserialize_repr)]
28#[repr(i32)]
29pub enum BybitUnifiedMarginStatus {
30 ClassicAccount = 1,
32 UnifiedTradingAccount10 = 3,
34 UnifiedTradingAccount10Pro = 4,
36 UnifiedTradingAccount20 = 5,
38 UnifiedTradingAccount20Pro = 6,
40}
41
42#[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#[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 MergedSingle = 0,
105 BothSides = 3,
107}
108
109#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize_repr, Deserialize_repr)]
111#[repr(i32)]
112pub enum BybitPositionIdx {
113 OneWay = 0,
115 BuyHedge = 1,
117 SellHedge = 2,
119}
120
121#[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#[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 Mainnet,
180 Demo,
182 Testnet,
184}
185
186#[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#[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#[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#[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 #[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 #[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 #[must_use]
286 pub fn is_spot(self) -> bool {
287 matches!(self, Self::Spot)
288 }
289
290 #[must_use]
292 pub fn is_linear(self) -> bool {
293 matches!(self, Self::Linear)
294 }
295
296 #[must_use]
298 pub fn is_inverse(self) -> bool {
299 matches!(self, Self::Inverse)
300 }
301
302 #[must_use]
304 pub fn is_option(self) -> bool {
305 matches!(self, Self::Option)
306 }
307}
308
309#[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#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
321#[serde(rename_all = "PascalCase")]
322pub enum BybitOptionType {
323 Call,
324 Put,
325}
326
327#[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#[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#[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 #[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 #[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, }
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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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 #[default]
821 OpenOnly = 0,
822 ClosedRecent = 1,
824}
825
826#[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 #[default]
842 Order,
843 StopOrder,
845 #[serde(rename = "tpslOrder")]
847 TpslOrder,
848 OcoOrder,
850 BidirectionalTpslOrder,
852}
853
854#[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,
886 Repay,
888 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}