1use 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#[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(eq, eq_int, module = "nautilus_trader.core.nautilus_pyo3.bybit")
62)]
63pub enum BybitMarginMode {
64 IsolatedMargin,
65 RegularMargin,
66 PortfolioMargin,
67}
68
69#[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 MergedSingle = 0,
93 BothSides = 3,
95}
96
97#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize_repr, Deserialize_repr)]
99#[repr(i32)]
100pub enum BybitPositionIdx {
101 OneWay = 0,
103 BuyHedge = 1,
105 SellHedge = 2,
107}
108
109#[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#[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 Mainnet,
156 Demo,
158 Testnet,
160}
161
162#[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#[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#[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#[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 #[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 #[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 #[must_use]
256 pub fn is_spot(self) -> bool {
257 matches!(self, Self::Spot)
258 }
259
260 #[must_use]
262 pub fn is_linear(self) -> bool {
263 matches!(self, Self::Linear)
264 }
265
266 #[must_use]
268 pub fn is_inverse(self) -> bool {
269 matches!(self, Self::Inverse)
270 }
271
272 #[must_use]
274 pub fn is_option(self) -> bool {
275 matches!(self, Self::Option)
276 }
277}
278
279#[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#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
291#[serde(rename_all = "PascalCase")]
292pub enum BybitOptionType {
293 Call,
294 Put,
295}
296
297#[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#[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#[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 #[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 #[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, }
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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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 #[default]
729 OpenOnly = 0,
730 ClosedRecent = 1,
732}
733
734#[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 #[default]
745 Order,
746 StopOrder,
748 #[serde(rename = "tpslOrder")]
750 TpslOrder,
751 OcoOrder,
753 BidirectionalTpslOrder,
755}
756
757#[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,
787 Repay,
789 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}