1use std::{convert::TryFrom, str::FromStr};
19
20use anyhow::Context;
21pub use nautilus_core::serialization::{
22 deserialize_decimal_or_zero, deserialize_optional_decimal,
23 deserialize_optional_decimal_or_zero, deserialize_string_to_u8,
24};
25use nautilus_core::{UUID4, datetime::NANOSECONDS_IN_MILLISECOND, nanos::UnixNanos};
26use nautilus_model::{
27 data::{Bar, BarType, TradeTick},
28 enums::{
29 AccountType, AggressorSide, AssetClass, BarAggregation, LiquiditySide, OptionKind,
30 OrderSide, OrderStatus, OrderType, PositionSideSpecified, TimeInForce, TriggerType,
31 },
32 events::account::state::AccountState,
33 identifiers::{AccountId, ClientOrderId, InstrumentId, Symbol, TradeId, Venue, VenueOrderId},
34 instruments::{
35 Instrument, any::InstrumentAny, crypto_future::CryptoFuture,
36 crypto_perpetual::CryptoPerpetual, currency_pair::CurrencyPair,
37 option_contract::OptionContract,
38 },
39 reports::{FillReport, OrderStatusReport, PositionStatusReport},
40 types::{AccountBalance, Currency, MarginBalance, Money, Price, Quantity},
41};
42use rust_decimal::Decimal;
43use ustr::Ustr;
44
45use crate::{
46 common::{
47 enums::{
48 BybitContractType, BybitKlineInterval, BybitOptionType, BybitOrderSide,
49 BybitOrderStatus, BybitOrderType, BybitPositionSide, BybitProductType,
50 BybitStopOrderType, BybitTimeInForce, BybitTriggerDirection,
51 },
52 symbol::BybitSymbol,
53 },
54 http::models::{
55 BybitExecution, BybitFeeRate, BybitInstrumentInverse, BybitInstrumentLinear,
56 BybitInstrumentOption, BybitInstrumentSpot, BybitKline, BybitPosition, BybitTrade,
57 BybitWalletBalance,
58 },
59};
60
61const BYBIT_MINUTE_INTERVALS: &[u64] = &[1, 3, 5, 15, 30, 60, 120, 240, 360, 720];
62const BYBIT_HOUR_INTERVALS: &[u64] = &[1, 2, 4, 6, 12];
63
64#[must_use]
66pub fn extract_raw_symbol(symbol: &str) -> &str {
67 symbol.rsplit_once('-').map_or(symbol, |(prefix, _)| prefix)
68}
69
70#[must_use]
74pub fn make_bybit_symbol<S: AsRef<str>>(raw_symbol: S, product_type: BybitProductType) -> Ustr {
75 let raw = raw_symbol.as_ref();
76 let suffix = match product_type {
77 BybitProductType::Spot => "-SPOT",
78 BybitProductType::Linear => "-LINEAR",
79 BybitProductType::Inverse => "-INVERSE",
80 BybitProductType::Option => "-OPTION",
81 };
82 Ustr::from(&format!("{raw}{suffix}"))
83}
84
85pub fn bar_spec_to_bybit_interval(
93 aggregation: BarAggregation,
94 step: u64,
95) -> anyhow::Result<BybitKlineInterval> {
96 match aggregation {
97 BarAggregation::Minute => match step {
98 1 => Ok(BybitKlineInterval::Minute1),
99 3 => Ok(BybitKlineInterval::Minute3),
100 5 => Ok(BybitKlineInterval::Minute5),
101 15 => Ok(BybitKlineInterval::Minute15),
102 30 => Ok(BybitKlineInterval::Minute30),
103 60 => Ok(BybitKlineInterval::Hour1),
105 120 => Ok(BybitKlineInterval::Hour2),
106 240 => Ok(BybitKlineInterval::Hour4),
107 360 => Ok(BybitKlineInterval::Hour6),
108 720 => Ok(BybitKlineInterval::Hour12),
109 _ => anyhow::bail!(
110 "Bybit only supports the following minute intervals: {BYBIT_MINUTE_INTERVALS:?}"
111 ),
112 },
113 BarAggregation::Hour => match step {
114 1 => Ok(BybitKlineInterval::Hour1),
115 2 => Ok(BybitKlineInterval::Hour2),
116 4 => Ok(BybitKlineInterval::Hour4),
117 6 => Ok(BybitKlineInterval::Hour6),
118 12 => Ok(BybitKlineInterval::Hour12),
119 _ => anyhow::bail!(
120 "Bybit only supports the following hour intervals: {BYBIT_HOUR_INTERVALS:?}"
121 ),
122 },
123 BarAggregation::Day => {
124 if step != 1 {
125 anyhow::bail!("Bybit only supports 1 DAY interval bars");
126 }
127 Ok(BybitKlineInterval::Day1)
128 }
129 BarAggregation::Week => {
130 if step != 1 {
131 anyhow::bail!("Bybit only supports 1 WEEK interval bars");
132 }
133 Ok(BybitKlineInterval::Week1)
134 }
135 BarAggregation::Month => {
136 if step != 1 {
137 anyhow::bail!("Bybit only supports 1 MONTH interval bars");
138 }
139 Ok(BybitKlineInterval::Month1)
140 }
141 _ => {
142 anyhow::bail!("Bybit does not support {aggregation:?} bars");
143 }
144 }
145}
146
147fn default_margin() -> Decimal {
148 Decimal::new(1, 1)
149}
150
151pub fn parse_spot_instrument(
153 definition: &BybitInstrumentSpot,
154 fee_rate: &BybitFeeRate,
155 ts_event: UnixNanos,
156 ts_init: UnixNanos,
157) -> anyhow::Result<InstrumentAny> {
158 let base_currency = get_currency(definition.base_coin.as_str());
159 let quote_currency = get_currency(definition.quote_coin.as_str());
160
161 let symbol = BybitSymbol::new(format!("{}-SPOT", definition.symbol))?;
162 let instrument_id = symbol.to_instrument_id();
163 let raw_symbol = Symbol::new(symbol.raw_symbol());
164
165 let price_increment = parse_price(&definition.price_filter.tick_size, "priceFilter.tickSize")?;
166 let size_increment = parse_quantity(
167 &definition.lot_size_filter.base_precision,
168 "lotSizeFilter.basePrecision",
169 )?;
170 let lot_size = Some(size_increment);
171 let max_quantity = Some(parse_quantity(
172 &definition.lot_size_filter.max_order_qty,
173 "lotSizeFilter.maxOrderQty",
174 )?);
175 let min_quantity = Some(parse_quantity(
176 &definition.lot_size_filter.min_order_qty,
177 "lotSizeFilter.minOrderQty",
178 )?);
179
180 let maker_fee = parse_decimal(&fee_rate.maker_fee_rate, "makerFeeRate")?;
181 let taker_fee = parse_decimal(&fee_rate.taker_fee_rate, "takerFeeRate")?;
182
183 let instrument = CurrencyPair::new(
184 instrument_id,
185 raw_symbol,
186 base_currency,
187 quote_currency,
188 price_increment.precision,
189 size_increment.precision,
190 price_increment,
191 size_increment,
192 None,
193 lot_size,
194 max_quantity,
195 min_quantity,
196 None,
197 None,
198 None,
199 None,
200 Some(default_margin()),
201 Some(default_margin()),
202 Some(maker_fee),
203 Some(taker_fee),
204 ts_event,
205 ts_init,
206 );
207
208 Ok(InstrumentAny::CurrencyPair(instrument))
209}
210
211pub fn parse_linear_instrument(
213 definition: &BybitInstrumentLinear,
214 fee_rate: &BybitFeeRate,
215 ts_event: UnixNanos,
216 ts_init: UnixNanos,
217) -> anyhow::Result<InstrumentAny> {
218 anyhow::ensure!(
220 !definition.base_coin.is_empty(),
221 "base_coin is empty for symbol '{}'",
222 definition.symbol
223 );
224 anyhow::ensure!(
225 !definition.quote_coin.is_empty(),
226 "quote_coin is empty for symbol '{}'",
227 definition.symbol
228 );
229
230 let base_currency = get_currency(definition.base_coin.as_str());
231 let quote_currency = get_currency(definition.quote_coin.as_str());
232 let settlement_currency = resolve_settlement_currency(
233 definition.settle_coin.as_str(),
234 base_currency,
235 quote_currency,
236 )?;
237
238 let symbol = BybitSymbol::new(format!("{}-LINEAR", definition.symbol))?;
239 let instrument_id = symbol.to_instrument_id();
240 let raw_symbol = Symbol::new(symbol.raw_symbol());
241
242 let price_increment = parse_price(&definition.price_filter.tick_size, "priceFilter.tickSize")?;
243 let size_increment = parse_quantity(
244 &definition.lot_size_filter.qty_step,
245 "lotSizeFilter.qtyStep",
246 )?;
247 let lot_size = Some(size_increment);
248 let max_quantity = Some(parse_quantity(
249 &definition.lot_size_filter.max_order_qty,
250 "lotSizeFilter.maxOrderQty",
251 )?);
252 let min_quantity = Some(parse_quantity(
253 &definition.lot_size_filter.min_order_qty,
254 "lotSizeFilter.minOrderQty",
255 )?);
256 let max_price = Some(parse_price(
257 &definition.price_filter.max_price,
258 "priceFilter.maxPrice",
259 )?);
260 let min_price = Some(parse_price(
261 &definition.price_filter.min_price,
262 "priceFilter.minPrice",
263 )?);
264
265 let maker_fee = parse_decimal(&fee_rate.maker_fee_rate, "makerFeeRate")?;
266 let taker_fee = parse_decimal(&fee_rate.taker_fee_rate, "takerFeeRate")?;
267
268 match definition.contract_type {
269 BybitContractType::LinearPerpetual => {
270 let instrument = CryptoPerpetual::new(
271 instrument_id,
272 raw_symbol,
273 base_currency,
274 quote_currency,
275 settlement_currency,
276 false,
277 price_increment.precision,
278 size_increment.precision,
279 price_increment,
280 size_increment,
281 None,
282 lot_size,
283 max_quantity,
284 min_quantity,
285 None,
286 None,
287 max_price,
288 min_price,
289 Some(default_margin()),
290 Some(default_margin()),
291 Some(maker_fee),
292 Some(taker_fee),
293 ts_event,
294 ts_init,
295 );
296 Ok(InstrumentAny::CryptoPerpetual(instrument))
297 }
298 BybitContractType::LinearFutures => {
299 let activation_ns = parse_millis_timestamp(&definition.launch_time, "launchTime")?;
300 let expiration_ns = parse_millis_timestamp(&definition.delivery_time, "deliveryTime")?;
301 let instrument = CryptoFuture::new(
302 instrument_id,
303 raw_symbol,
304 base_currency,
305 quote_currency,
306 settlement_currency,
307 false,
308 activation_ns,
309 expiration_ns,
310 price_increment.precision,
311 size_increment.precision,
312 price_increment,
313 size_increment,
314 None,
315 lot_size,
316 max_quantity,
317 min_quantity,
318 None,
319 None,
320 max_price,
321 min_price,
322 Some(default_margin()),
323 Some(default_margin()),
324 Some(maker_fee),
325 Some(taker_fee),
326 ts_event,
327 ts_init,
328 );
329 Ok(InstrumentAny::CryptoFuture(instrument))
330 }
331 other => Err(anyhow::anyhow!(
332 "unsupported linear contract variant: {other:?}"
333 )),
334 }
335}
336
337pub fn parse_inverse_instrument(
339 definition: &BybitInstrumentInverse,
340 fee_rate: &BybitFeeRate,
341 ts_event: UnixNanos,
342 ts_init: UnixNanos,
343) -> anyhow::Result<InstrumentAny> {
344 anyhow::ensure!(
346 !definition.base_coin.is_empty(),
347 "base_coin is empty for symbol '{}'",
348 definition.symbol
349 );
350 anyhow::ensure!(
351 !definition.quote_coin.is_empty(),
352 "quote_coin is empty for symbol '{}'",
353 definition.symbol
354 );
355
356 let base_currency = get_currency(definition.base_coin.as_str());
357 let quote_currency = get_currency(definition.quote_coin.as_str());
358 let settlement_currency = resolve_settlement_currency(
359 definition.settle_coin.as_str(),
360 base_currency,
361 quote_currency,
362 )?;
363
364 let symbol = BybitSymbol::new(format!("{}-INVERSE", definition.symbol))?;
365 let instrument_id = symbol.to_instrument_id();
366 let raw_symbol = Symbol::new(symbol.raw_symbol());
367
368 let price_increment = parse_price(&definition.price_filter.tick_size, "priceFilter.tickSize")?;
369 let size_increment = parse_quantity(
370 &definition.lot_size_filter.qty_step,
371 "lotSizeFilter.qtyStep",
372 )?;
373 let lot_size = Some(size_increment);
374 let max_quantity = Some(parse_quantity(
375 &definition.lot_size_filter.max_order_qty,
376 "lotSizeFilter.maxOrderQty",
377 )?);
378 let min_quantity = Some(parse_quantity(
379 &definition.lot_size_filter.min_order_qty,
380 "lotSizeFilter.minOrderQty",
381 )?);
382 let max_price = Some(parse_price(
383 &definition.price_filter.max_price,
384 "priceFilter.maxPrice",
385 )?);
386 let min_price = Some(parse_price(
387 &definition.price_filter.min_price,
388 "priceFilter.minPrice",
389 )?);
390
391 let maker_fee = parse_decimal(&fee_rate.maker_fee_rate, "makerFeeRate")?;
392 let taker_fee = parse_decimal(&fee_rate.taker_fee_rate, "takerFeeRate")?;
393
394 match definition.contract_type {
395 BybitContractType::InversePerpetual => {
396 let instrument = CryptoPerpetual::new(
397 instrument_id,
398 raw_symbol,
399 base_currency,
400 quote_currency,
401 settlement_currency,
402 true,
403 price_increment.precision,
404 size_increment.precision,
405 price_increment,
406 size_increment,
407 None,
408 lot_size,
409 max_quantity,
410 min_quantity,
411 None,
412 None,
413 max_price,
414 min_price,
415 Some(default_margin()),
416 Some(default_margin()),
417 Some(maker_fee),
418 Some(taker_fee),
419 ts_event,
420 ts_init,
421 );
422 Ok(InstrumentAny::CryptoPerpetual(instrument))
423 }
424 BybitContractType::InverseFutures => {
425 let activation_ns = parse_millis_timestamp(&definition.launch_time, "launchTime")?;
426 let expiration_ns = parse_millis_timestamp(&definition.delivery_time, "deliveryTime")?;
427 let instrument = CryptoFuture::new(
428 instrument_id,
429 raw_symbol,
430 base_currency,
431 quote_currency,
432 settlement_currency,
433 true,
434 activation_ns,
435 expiration_ns,
436 price_increment.precision,
437 size_increment.precision,
438 price_increment,
439 size_increment,
440 None,
441 lot_size,
442 max_quantity,
443 min_quantity,
444 None,
445 None,
446 max_price,
447 min_price,
448 Some(default_margin()),
449 Some(default_margin()),
450 Some(maker_fee),
451 Some(taker_fee),
452 ts_event,
453 ts_init,
454 );
455 Ok(InstrumentAny::CryptoFuture(instrument))
456 }
457 other => Err(anyhow::anyhow!(
458 "unsupported inverse contract variant: {other:?}"
459 )),
460 }
461}
462
463pub fn parse_option_instrument(
465 definition: &BybitInstrumentOption,
466 ts_event: UnixNanos,
467 ts_init: UnixNanos,
468) -> anyhow::Result<InstrumentAny> {
469 let quote_currency = get_currency(definition.quote_coin.as_str());
470
471 let symbol = BybitSymbol::new(format!("{}-OPTION", definition.symbol))?;
472 let instrument_id = symbol.to_instrument_id();
473 let raw_symbol = Symbol::new(symbol.raw_symbol());
474
475 let price_increment = parse_price(&definition.price_filter.tick_size, "priceFilter.tickSize")?;
476 let max_price = Some(parse_price(
477 &definition.price_filter.max_price,
478 "priceFilter.maxPrice",
479 )?);
480 let min_price = Some(parse_price(
481 &definition.price_filter.min_price,
482 "priceFilter.minPrice",
483 )?);
484 let lot_size = parse_quantity(
485 &definition.lot_size_filter.qty_step,
486 "lotSizeFilter.qtyStep",
487 )?;
488 let max_quantity = Some(parse_quantity(
489 &definition.lot_size_filter.max_order_qty,
490 "lotSizeFilter.maxOrderQty",
491 )?);
492 let min_quantity = Some(parse_quantity(
493 &definition.lot_size_filter.min_order_qty,
494 "lotSizeFilter.minOrderQty",
495 )?);
496
497 let option_kind = match definition.options_type {
498 BybitOptionType::Call => OptionKind::Call,
499 BybitOptionType::Put => OptionKind::Put,
500 };
501
502 let strike_price = extract_strike_from_symbol(&definition.symbol)?;
503 let activation_ns = parse_millis_timestamp(&definition.launch_time, "launchTime")?;
504 let expiration_ns = parse_millis_timestamp(&definition.delivery_time, "deliveryTime")?;
505
506 let instrument = OptionContract::new(
507 instrument_id,
508 raw_symbol,
509 AssetClass::Cryptocurrency,
510 None,
511 definition.base_coin,
512 option_kind,
513 strike_price,
514 quote_currency,
515 activation_ns,
516 expiration_ns,
517 price_increment.precision,
518 price_increment,
519 Quantity::from(1_u32),
520 lot_size,
521 max_quantity,
522 min_quantity,
523 max_price,
524 min_price,
525 Some(Decimal::ZERO),
526 Some(Decimal::ZERO),
527 Some(Decimal::ZERO),
528 Some(Decimal::ZERO),
529 ts_event,
530 ts_init,
531 );
532
533 Ok(InstrumentAny::OptionContract(instrument))
534}
535
536pub fn parse_trade_tick(
538 trade: &BybitTrade,
539 instrument: &InstrumentAny,
540 ts_init: UnixNanos,
541) -> anyhow::Result<TradeTick> {
542 let price =
543 parse_price_with_precision(&trade.price, instrument.price_precision(), "trade.price")?;
544 let size =
545 parse_quantity_with_precision(&trade.size, instrument.size_precision(), "trade.size")?;
546 let aggressor: AggressorSide = trade.side.into();
547 let trade_id = TradeId::new_checked(trade.exec_id.as_str())
548 .context("invalid exec_id in Bybit trade payload")?;
549 let ts_event = parse_millis_timestamp(&trade.time, "trade.time")?;
550
551 TradeTick::new_checked(
552 instrument.id(),
553 price,
554 size,
555 aggressor,
556 trade_id,
557 ts_event,
558 ts_init,
559 )
560 .context("failed to construct TradeTick from Bybit trade payload")
561}
562
563pub fn parse_kline_bar(
565 kline: &BybitKline,
566 instrument: &InstrumentAny,
567 bar_type: BarType,
568 timestamp_on_close: bool,
569 ts_init: UnixNanos,
570) -> anyhow::Result<Bar> {
571 let price_precision = instrument.price_precision();
572 let size_precision = instrument.size_precision();
573
574 let open = parse_price_with_precision(&kline.open, price_precision, "kline.open")?;
575 let high = parse_price_with_precision(&kline.high, price_precision, "kline.high")?;
576 let low = parse_price_with_precision(&kline.low, price_precision, "kline.low")?;
577 let close = parse_price_with_precision(&kline.close, price_precision, "kline.close")?;
578 let volume = parse_quantity_with_precision(&kline.volume, size_precision, "kline.volume")?;
579
580 let mut ts_event = parse_millis_timestamp(&kline.start, "kline.start")?;
581 if timestamp_on_close {
582 let interval_ns = bar_type
583 .spec()
584 .timedelta()
585 .num_nanoseconds()
586 .context("bar specification produced non-integer interval")?;
587 let interval_ns = u64::try_from(interval_ns)
588 .context("bar interval overflowed the u64 range for nanoseconds")?;
589 let updated = ts_event
590 .as_u64()
591 .checked_add(interval_ns)
592 .context("bar timestamp overflowed when adjusting to close time")?;
593 ts_event = UnixNanos::from(updated);
594 }
595 let ts_init = if ts_init.is_zero() { ts_event } else { ts_init };
596
597 Bar::new_checked(bar_type, open, high, low, close, volume, ts_event, ts_init)
598 .context("failed to construct Bar from Bybit kline entry")
599}
600
601pub fn parse_fill_report(
610 execution: &BybitExecution,
611 account_id: AccountId,
612 instrument: &InstrumentAny,
613 ts_init: UnixNanos,
614) -> anyhow::Result<FillReport> {
615 let instrument_id = instrument.id();
616 let venue_order_id = VenueOrderId::new(execution.order_id.as_str());
617 let trade_id = TradeId::new_checked(execution.exec_id.as_str())
618 .context("invalid execId in Bybit execution payload")?;
619
620 let order_side: OrderSide = execution.side.into();
621
622 let last_px = parse_price_with_precision(
623 &execution.exec_price,
624 instrument.price_precision(),
625 "execution.execPrice",
626 )?;
627
628 let last_qty = parse_quantity_with_precision(
629 &execution.exec_qty,
630 instrument.size_precision(),
631 "execution.execQty",
632 )?;
633
634 let fee_f64 = execution
636 .exec_fee
637 .parse::<f64>()
638 .with_context(|| format!("Failed to parse execFee='{}'", execution.exec_fee))?;
639 let currency = get_currency(&execution.fee_currency);
640 let commission = Money::new(-fee_f64, currency);
641
642 let liquidity_side = if execution.is_maker {
644 LiquiditySide::Maker
645 } else {
646 LiquiditySide::Taker
647 };
648
649 let ts_event = parse_millis_timestamp(&execution.exec_time, "execution.execTime")?;
650
651 let client_order_id = if execution.order_link_id.is_empty() {
653 None
654 } else {
655 Some(ClientOrderId::new(execution.order_link_id.as_str()))
656 };
657
658 Ok(FillReport::new(
659 account_id,
660 instrument_id,
661 venue_order_id,
662 trade_id,
663 order_side,
664 last_qty,
665 last_px,
666 commission,
667 liquidity_side,
668 client_order_id,
669 None, ts_event,
671 ts_init,
672 None, ))
674}
675
676pub fn parse_position_status_report(
685 position: &BybitPosition,
686 account_id: AccountId,
687 instrument: &InstrumentAny,
688 ts_init: UnixNanos,
689) -> anyhow::Result<PositionStatusReport> {
690 let instrument_id = instrument.id();
691
692 let size_f64 = position
694 .size
695 .parse::<f64>()
696 .with_context(|| format!("Failed to parse position size '{}'", position.size))?;
697
698 let (position_side, quantity) = match position.side {
700 BybitPositionSide::Buy => {
701 let qty = Quantity::new(size_f64, instrument.size_precision());
702 (PositionSideSpecified::Long, qty)
703 }
704 BybitPositionSide::Sell => {
705 let qty = Quantity::new(size_f64, instrument.size_precision());
706 (PositionSideSpecified::Short, qty)
707 }
708 BybitPositionSide::Flat => {
709 let qty = Quantity::new(0.0, instrument.size_precision());
710 (PositionSideSpecified::Flat, qty)
711 }
712 };
713
714 let avg_px_open = if position.avg_price.is_empty() || position.avg_price == "0" {
716 None
717 } else {
718 Some(Decimal::from_str(&position.avg_price)?)
719 };
720
721 let ts_last = if position.updated_time.is_empty() {
723 ts_init
724 } else {
725 parse_millis_timestamp(&position.updated_time, "position.updatedTime")?
726 };
727
728 Ok(PositionStatusReport::new(
729 account_id,
730 instrument_id,
731 position_side,
732 quantity,
733 ts_last,
734 ts_init,
735 None, None, avg_px_open,
738 ))
739}
740
741pub fn parse_account_state(
749 wallet_balance: &BybitWalletBalance,
750 account_id: AccountId,
751 ts_init: UnixNanos,
752) -> anyhow::Result<AccountState> {
753 let mut balances = Vec::new();
754
755 for coin in &wallet_balance.coin {
756 let total_dec = coin.wallet_balance - coin.spot_borrow;
757 let locked_dec = coin.locked;
758
759 let currency = get_currency(&coin.coin);
760 let total = Money::from_decimal(total_dec, currency)?;
761 let locked = Money::from_decimal(locked_dec, currency)?;
762 let free = Money::from_raw(total.raw - locked.raw, currency);
763
764 balances.push(AccountBalance::new(total, locked, free));
765 }
766
767 let mut margins = Vec::new();
768
769 for coin in &wallet_balance.coin {
770 let initial_margin_f64 = match &coin.total_position_im {
771 Some(im) if !im.is_empty() => im.parse::<f64>()?,
772 _ => 0.0,
773 };
774
775 let maintenance_margin_f64 = match &coin.total_position_mm {
776 Some(mm) if !mm.is_empty() => mm.parse::<f64>()?,
777 _ => 0.0,
778 };
779
780 let currency = get_currency(&coin.coin);
781
782 if initial_margin_f64 > 0.0 || maintenance_margin_f64 > 0.0 {
784 let initial_margin = Money::new(initial_margin_f64, currency);
785 let maintenance_margin = Money::new(maintenance_margin_f64, currency);
786
787 let margin_instrument_id = InstrumentId::new(
789 Symbol::from_str_unchecked(format!("ACCOUNT-{}", coin.coin)),
790 Venue::new("BYBIT"),
791 );
792
793 margins.push(MarginBalance::new(
794 initial_margin,
795 maintenance_margin,
796 margin_instrument_id,
797 ));
798 }
799 }
800
801 let account_type = AccountType::Margin;
802 let is_reported = true;
803 let event_id = UUID4::new();
804
805 let ts_event = ts_init;
807
808 Ok(AccountState::new(
809 account_id,
810 account_type,
811 balances,
812 margins,
813 is_reported,
814 event_id,
815 ts_event,
816 ts_init,
817 None,
818 ))
819}
820
821pub(crate) fn parse_price_with_precision(
822 value: &str,
823 precision: u8,
824 field: &str,
825) -> anyhow::Result<Price> {
826 let parsed = value
827 .parse::<f64>()
828 .with_context(|| format!("Failed to parse {field}='{value}' as f64"))?;
829 Price::new_checked(parsed, precision).with_context(|| {
830 format!("Failed to construct Price for {field} with precision {precision}")
831 })
832}
833
834pub(crate) fn parse_quantity_with_precision(
835 value: &str,
836 precision: u8,
837 field: &str,
838) -> anyhow::Result<Quantity> {
839 let parsed = value
840 .parse::<f64>()
841 .with_context(|| format!("Failed to parse {field}='{value}' as f64"))?;
842 Quantity::new_checked(parsed, precision).with_context(|| {
843 format!("Failed to construct Quantity for {field} with precision {precision}")
844 })
845}
846
847pub(crate) fn parse_price(value: &str, field: &str) -> anyhow::Result<Price> {
848 Price::from_str(value)
849 .map_err(|err| anyhow::anyhow!("Failed to parse {field}='{value}': {err}"))
850}
851
852pub(crate) fn parse_quantity(value: &str, field: &str) -> anyhow::Result<Quantity> {
853 Quantity::from_str(value)
854 .map_err(|err| anyhow::anyhow!("Failed to parse {field}='{value}': {err}"))
855}
856
857pub(crate) fn parse_decimal(value: &str, field: &str) -> anyhow::Result<Decimal> {
858 Decimal::from_str(value)
859 .map_err(|err| anyhow::anyhow!("Failed to parse {field}='{value}' as Decimal: {err}"))
860}
861
862pub(crate) fn parse_millis_timestamp(value: &str, field: &str) -> anyhow::Result<UnixNanos> {
863 let millis: u64 = value
864 .parse()
865 .with_context(|| format!("Failed to parse {field}='{value}' as u64 millis"))?;
866 let nanos = millis
867 .checked_mul(NANOSECONDS_IN_MILLISECOND)
868 .context("millisecond timestamp overflowed when converting to nanoseconds")?;
869 Ok(UnixNanos::from(nanos))
870}
871
872fn resolve_settlement_currency(
873 settle_coin: &str,
874 base_currency: Currency,
875 quote_currency: Currency,
876) -> anyhow::Result<Currency> {
877 if settle_coin.eq_ignore_ascii_case(base_currency.code.as_str()) {
878 Ok(base_currency)
879 } else if settle_coin.eq_ignore_ascii_case(quote_currency.code.as_str()) {
880 Ok(quote_currency)
881 } else {
882 Err(anyhow::anyhow!(
883 "unrecognised settlement currency '{settle_coin}'"
884 ))
885 }
886}
887
888pub fn get_currency(code: &str) -> Currency {
893 Currency::get_or_create_crypto(code)
894}
895
896fn extract_strike_from_symbol(symbol: &str) -> anyhow::Result<Price> {
897 let parts: Vec<&str> = symbol.split('-').collect();
898 let strike = parts
899 .get(2)
900 .ok_or_else(|| anyhow::anyhow!("invalid option symbol '{symbol}'"))?;
901 parse_price(strike, "option strike")
902}
903
904pub fn parse_order_status_report(
906 order: &crate::http::models::BybitOrder,
907 instrument: &InstrumentAny,
908 account_id: AccountId,
909 ts_init: UnixNanos,
910) -> anyhow::Result<OrderStatusReport> {
911 let instrument_id = instrument.id();
912 let venue_order_id = VenueOrderId::new(order.order_id);
913
914 let order_side: OrderSide = order.side.into();
915
916 let order_type: OrderType = match (
918 order.order_type,
919 order.stop_order_type,
920 order.trigger_direction,
921 order.side,
922 ) {
923 (BybitOrderType::Market, BybitStopOrderType::None | BybitStopOrderType::Unknown, _, _) => {
924 OrderType::Market
925 }
926 (BybitOrderType::Limit, BybitStopOrderType::None | BybitStopOrderType::Unknown, _, _) => {
927 OrderType::Limit
928 }
929
930 (
931 BybitOrderType::Market,
932 BybitStopOrderType::Stop,
933 BybitTriggerDirection::RisesTo,
934 BybitOrderSide::Buy,
935 ) => OrderType::StopMarket,
936 (
937 BybitOrderType::Market,
938 BybitStopOrderType::Stop,
939 BybitTriggerDirection::FallsTo,
940 BybitOrderSide::Buy,
941 ) => OrderType::MarketIfTouched,
942
943 (
944 BybitOrderType::Market,
945 BybitStopOrderType::Stop,
946 BybitTriggerDirection::FallsTo,
947 BybitOrderSide::Sell,
948 ) => OrderType::StopMarket,
949 (
950 BybitOrderType::Market,
951 BybitStopOrderType::Stop,
952 BybitTriggerDirection::RisesTo,
953 BybitOrderSide::Sell,
954 ) => OrderType::MarketIfTouched,
955
956 (
957 BybitOrderType::Limit,
958 BybitStopOrderType::Stop,
959 BybitTriggerDirection::RisesTo,
960 BybitOrderSide::Buy,
961 ) => OrderType::StopLimit,
962 (
963 BybitOrderType::Limit,
964 BybitStopOrderType::Stop,
965 BybitTriggerDirection::FallsTo,
966 BybitOrderSide::Buy,
967 ) => OrderType::LimitIfTouched,
968
969 (
970 BybitOrderType::Limit,
971 BybitStopOrderType::Stop,
972 BybitTriggerDirection::FallsTo,
973 BybitOrderSide::Sell,
974 ) => OrderType::StopLimit,
975 (
976 BybitOrderType::Limit,
977 BybitStopOrderType::Stop,
978 BybitTriggerDirection::RisesTo,
979 BybitOrderSide::Sell,
980 ) => OrderType::LimitIfTouched,
981
982 (BybitOrderType::Market, BybitStopOrderType::Stop, BybitTriggerDirection::None, _) => {
984 OrderType::Market
985 }
986 (BybitOrderType::Limit, BybitStopOrderType::Stop, BybitTriggerDirection::None, _) => {
987 OrderType::Limit
988 }
989
990 (BybitOrderType::Market, _, _, _) => OrderType::Market,
992 (BybitOrderType::Limit, _, _, _) => OrderType::Limit,
993
994 (BybitOrderType::Unknown, _, _, _) => OrderType::Limit,
995 };
996
997 let time_in_force: TimeInForce = match order.time_in_force {
998 BybitTimeInForce::Gtc => TimeInForce::Gtc,
999 BybitTimeInForce::Ioc => TimeInForce::Ioc,
1000 BybitTimeInForce::Fok => TimeInForce::Fok,
1001 BybitTimeInForce::PostOnly => TimeInForce::Gtc,
1002 };
1003
1004 let quantity =
1005 parse_quantity_with_precision(&order.qty, instrument.size_precision(), "order.qty")?;
1006
1007 let filled_qty = parse_quantity_with_precision(
1008 &order.cum_exec_qty,
1009 instrument.size_precision(),
1010 "order.cumExecQty",
1011 )?;
1012
1013 let order_status: OrderStatus = match order.order_status {
1019 BybitOrderStatus::Created | BybitOrderStatus::New | BybitOrderStatus::Untriggered => {
1020 OrderStatus::Accepted
1021 }
1022 BybitOrderStatus::Rejected => {
1023 if filled_qty.is_positive() {
1024 OrderStatus::Canceled
1025 } else {
1026 OrderStatus::Rejected
1027 }
1028 }
1029 BybitOrderStatus::PartiallyFilled => OrderStatus::PartiallyFilled,
1030 BybitOrderStatus::Filled => OrderStatus::Filled,
1031 BybitOrderStatus::Canceled | BybitOrderStatus::PartiallyFilledCanceled => {
1032 OrderStatus::Canceled
1033 }
1034 BybitOrderStatus::Triggered => OrderStatus::Triggered,
1035 BybitOrderStatus::Deactivated => OrderStatus::Canceled,
1036 };
1037
1038 let ts_accepted = parse_millis_timestamp(&order.created_time, "order.createdTime")?;
1039 let ts_last = parse_millis_timestamp(&order.updated_time, "order.updatedTime")?;
1040
1041 let mut report = OrderStatusReport::new(
1042 account_id,
1043 instrument_id,
1044 None,
1045 venue_order_id,
1046 order_side,
1047 order_type,
1048 time_in_force,
1049 order_status,
1050 quantity,
1051 filled_qty,
1052 ts_accepted,
1053 ts_last,
1054 ts_init,
1055 Some(UUID4::new()),
1056 );
1057
1058 if !order.order_link_id.is_empty() {
1059 report = report.with_client_order_id(ClientOrderId::new(order.order_link_id.as_str()));
1060 }
1061
1062 if !order.price.is_empty() && order.price != "0" {
1063 let price =
1064 parse_price_with_precision(&order.price, instrument.price_precision(), "order.price")?;
1065 report = report.with_price(price);
1066 }
1067
1068 if let Some(avg_price) = &order.avg_price
1069 && !avg_price.is_empty()
1070 && avg_price != "0"
1071 {
1072 let avg_px = avg_price
1073 .parse::<f64>()
1074 .with_context(|| format!("Failed to parse avg_price='{avg_price}' as f64"))?;
1075 report = report.with_avg_px(avg_px)?;
1076 }
1077
1078 if !order.trigger_price.is_empty() && order.trigger_price != "0" {
1079 let trigger_price = parse_price_with_precision(
1080 &order.trigger_price,
1081 instrument.price_precision(),
1082 "order.triggerPrice",
1083 )?;
1084 report = report.with_trigger_price(trigger_price);
1085
1086 let trigger_type: TriggerType = order.trigger_by.into();
1088 report = report.with_trigger_type(trigger_type);
1089 }
1090
1091 Ok(report)
1092}
1093
1094#[cfg(test)]
1095mod tests {
1096 use nautilus_model::{
1097 data::BarSpecification,
1098 enums::{AggregationSource, BarAggregation, PositionSide, PriceType},
1099 };
1100 use rstest::rstest;
1101
1102 use super::*;
1103 use crate::{
1104 common::testing::load_test_json,
1105 http::models::{
1106 BybitInstrumentInverseResponse, BybitInstrumentLinearResponse,
1107 BybitInstrumentOptionResponse, BybitInstrumentSpotResponse, BybitKlinesResponse,
1108 BybitTradesResponse,
1109 },
1110 };
1111
1112 const TS: UnixNanos = UnixNanos::new(1_700_000_000_000_000_000);
1113
1114 fn sample_fee_rate(
1115 symbol: &str,
1116 taker: &str,
1117 maker: &str,
1118 base_coin: Option<&str>,
1119 ) -> BybitFeeRate {
1120 BybitFeeRate {
1121 symbol: Ustr::from(symbol),
1122 taker_fee_rate: taker.to_string(),
1123 maker_fee_rate: maker.to_string(),
1124 base_coin: base_coin.map(Ustr::from),
1125 }
1126 }
1127
1128 fn linear_instrument() -> InstrumentAny {
1129 let json = load_test_json("http_get_instruments_linear.json");
1130 let response: BybitInstrumentLinearResponse = serde_json::from_str(&json).unwrap();
1131 let instrument = &response.result.list[0];
1132 let fee_rate = sample_fee_rate("BTCUSDT", "0.00055", "0.0001", Some("BTC"));
1133 parse_linear_instrument(instrument, &fee_rate, TS, TS).unwrap()
1134 }
1135
1136 #[rstest]
1137 fn parse_spot_instrument_builds_currency_pair() {
1138 let json = load_test_json("http_get_instruments_spot.json");
1139 let response: BybitInstrumentSpotResponse = serde_json::from_str(&json).unwrap();
1140 let instrument = &response.result.list[0];
1141 let fee_rate = sample_fee_rate("BTCUSDT", "0.0006", "0.0001", Some("BTC"));
1142
1143 let parsed = parse_spot_instrument(instrument, &fee_rate, TS, TS).unwrap();
1144 match parsed {
1145 InstrumentAny::CurrencyPair(pair) => {
1146 assert_eq!(pair.id.to_string(), "BTCUSDT-SPOT.BYBIT");
1147 assert_eq!(pair.price_increment, Price::from_str("0.1").unwrap());
1148 assert_eq!(pair.size_increment, Quantity::from_str("0.0001").unwrap());
1149 assert_eq!(pair.base_currency.code.as_str(), "BTC");
1150 assert_eq!(pair.quote_currency.code.as_str(), "USDT");
1151 }
1152 _ => panic!("expected CurrencyPair"),
1153 }
1154 }
1155
1156 #[rstest]
1157 fn parse_linear_perpetual_instrument_builds_crypto_perpetual() {
1158 let json = load_test_json("http_get_instruments_linear.json");
1159 let response: BybitInstrumentLinearResponse = serde_json::from_str(&json).unwrap();
1160 let instrument = &response.result.list[0];
1161 let fee_rate = sample_fee_rate("BTCUSDT", "0.00055", "0.0001", Some("BTC"));
1162
1163 let parsed = parse_linear_instrument(instrument, &fee_rate, TS, TS).unwrap();
1164 match parsed {
1165 InstrumentAny::CryptoPerpetual(perp) => {
1166 assert_eq!(perp.id.to_string(), "BTCUSDT-LINEAR.BYBIT");
1167 assert!(!perp.is_inverse);
1168 assert_eq!(perp.price_increment, Price::from_str("0.5").unwrap());
1169 assert_eq!(perp.size_increment, Quantity::from_str("0.001").unwrap());
1170 }
1171 other => panic!("unexpected instrument variant: {other:?}"),
1172 }
1173 }
1174
1175 #[rstest]
1176 fn parse_inverse_perpetual_instrument_builds_inverse_perpetual() {
1177 let json = load_test_json("http_get_instruments_inverse.json");
1178 let response: BybitInstrumentInverseResponse = serde_json::from_str(&json).unwrap();
1179 let instrument = &response.result.list[0];
1180 let fee_rate = sample_fee_rate("BTCUSD", "0.00075", "0.00025", Some("BTC"));
1181
1182 let parsed = parse_inverse_instrument(instrument, &fee_rate, TS, TS).unwrap();
1183 match parsed {
1184 InstrumentAny::CryptoPerpetual(perp) => {
1185 assert_eq!(perp.id.to_string(), "BTCUSD-INVERSE.BYBIT");
1186 assert!(perp.is_inverse);
1187 assert_eq!(perp.price_increment, Price::from_str("0.5").unwrap());
1188 assert_eq!(perp.size_increment, Quantity::from_str("1").unwrap());
1189 }
1190 other => panic!("unexpected instrument variant: {other:?}"),
1191 }
1192 }
1193
1194 #[rstest]
1195 fn parse_option_instrument_builds_option_contract() {
1196 let json = load_test_json("http_get_instruments_option.json");
1197 let response: BybitInstrumentOptionResponse = serde_json::from_str(&json).unwrap();
1198 let instrument = &response.result.list[0];
1199
1200 let parsed = parse_option_instrument(instrument, TS, TS).unwrap();
1201 match parsed {
1202 InstrumentAny::OptionContract(option) => {
1203 assert_eq!(option.id.to_string(), "ETH-26JUN26-16000-P-OPTION.BYBIT");
1204 assert_eq!(option.option_kind, OptionKind::Put);
1205 assert_eq!(option.price_increment, Price::from_str("0.1").unwrap());
1206 assert_eq!(option.lot_size, Quantity::from_str("1").unwrap());
1207 }
1208 other => panic!("unexpected instrument variant: {other:?}"),
1209 }
1210 }
1211
1212 #[rstest]
1213 fn parse_http_trade_into_trade_tick() {
1214 let instrument = linear_instrument();
1215 let json = load_test_json("http_get_trades_recent.json");
1216 let response: BybitTradesResponse = serde_json::from_str(&json).unwrap();
1217 let trade = &response.result.list[0];
1218
1219 let tick = parse_trade_tick(trade, &instrument, TS).unwrap();
1220
1221 assert_eq!(tick.instrument_id, instrument.id());
1222 assert_eq!(tick.price, instrument.make_price(27450.50));
1223 assert_eq!(tick.size, instrument.make_qty(0.005, None));
1224 assert_eq!(tick.aggressor_side, AggressorSide::Buyer);
1225 assert_eq!(
1226 tick.trade_id.to_string(),
1227 "a905d5c3-1ed0-4f37-83e4-9c73a2fe2f01"
1228 );
1229 assert_eq!(tick.ts_event, UnixNanos::new(1_709_891_679_000_000_000));
1230 }
1231
1232 #[rstest]
1233 fn parse_kline_into_bar() {
1234 let instrument = linear_instrument();
1235 let json = load_test_json("http_get_klines_linear.json");
1236 let response: BybitKlinesResponse = serde_json::from_str(&json).unwrap();
1237 let kline = &response.result.list[0];
1238
1239 let bar_type = BarType::new(
1240 instrument.id(),
1241 BarSpecification::new(1, BarAggregation::Minute, PriceType::Last),
1242 AggregationSource::External,
1243 );
1244
1245 let bar = parse_kline_bar(kline, &instrument, bar_type, false, TS).unwrap();
1246
1247 assert_eq!(bar.bar_type.to_string(), bar_type.to_string());
1248 assert_eq!(bar.open, instrument.make_price(27450.0));
1249 assert_eq!(bar.high, instrument.make_price(27460.0));
1250 assert_eq!(bar.low, instrument.make_price(27440.0));
1251 assert_eq!(bar.close, instrument.make_price(27455.0));
1252 assert_eq!(bar.volume, instrument.make_qty(123.45, None));
1253 assert_eq!(bar.ts_event, UnixNanos::new(1_709_891_679_000_000_000));
1254 }
1255
1256 #[rstest]
1257 fn parse_http_position_short_into_position_status_report() {
1258 use crate::http::models::BybitPositionListResponse;
1259
1260 let json = load_test_json("http_get_positions.json");
1261 let response: BybitPositionListResponse = serde_json::from_str(&json).unwrap();
1262
1263 let short_position = &response.result.list[1];
1265 assert_eq!(short_position.symbol.as_str(), "ETHUSDT");
1266 assert_eq!(short_position.side, BybitPositionSide::Sell);
1267
1268 let eth_json = load_test_json("http_get_instruments_linear.json");
1270 let eth_response: BybitInstrumentLinearResponse = serde_json::from_str(ð_json).unwrap();
1271 let eth_def = ð_response.result.list[1]; let fee_rate = sample_fee_rate("ETHUSDT", "0.00055", "0.0001", Some("ETH"));
1273 let eth_instrument = parse_linear_instrument(eth_def, &fee_rate, TS, TS).unwrap();
1274
1275 let account_id = AccountId::new("BYBIT-001");
1276 let report =
1277 parse_position_status_report(short_position, account_id, ð_instrument, TS).unwrap();
1278
1279 assert_eq!(report.account_id, account_id);
1281 assert_eq!(report.instrument_id.symbol.as_str(), "ETHUSDT-LINEAR");
1282 assert_eq!(report.position_side.as_position_side(), PositionSide::Short);
1283 assert_eq!(report.quantity, eth_instrument.make_qty(5.0, None));
1284 assert_eq!(
1285 report.avg_px_open,
1286 Some(Decimal::try_from(3000.00).unwrap())
1287 );
1288 assert_eq!(report.ts_last, UnixNanos::new(1_697_673_700_112_000_000));
1289 }
1290
1291 #[rstest]
1292 fn parse_http_order_partially_filled_rejected_maps_to_canceled() {
1293 use crate::http::models::BybitOrderHistoryResponse;
1294
1295 let instrument = linear_instrument();
1296 let json = load_test_json("http_get_order_partially_filled_rejected.json");
1297 let response: BybitOrderHistoryResponse = serde_json::from_str(&json).unwrap();
1298 let order = &response.result.list[0];
1299 let account_id = AccountId::new("BYBIT-001");
1300
1301 let report = parse_order_status_report(order, &instrument, account_id, TS).unwrap();
1302
1303 assert_eq!(report.order_status, OrderStatus::Canceled);
1305 assert_eq!(report.filled_qty, instrument.make_qty(0.005, None));
1306 assert_eq!(
1307 report.client_order_id.as_ref().unwrap().to_string(),
1308 "O-20251001-164609-APEX-000-49"
1309 );
1310 }
1311
1312 #[rstest]
1313 #[case(BarAggregation::Minute, 1, BybitKlineInterval::Minute1)]
1314 #[case(BarAggregation::Minute, 3, BybitKlineInterval::Minute3)]
1315 #[case(BarAggregation::Minute, 5, BybitKlineInterval::Minute5)]
1316 #[case(BarAggregation::Minute, 15, BybitKlineInterval::Minute15)]
1317 #[case(BarAggregation::Minute, 30, BybitKlineInterval::Minute30)]
1318 #[case(BarAggregation::Minute, 60, BybitKlineInterval::Hour1)]
1319 #[case(BarAggregation::Minute, 120, BybitKlineInterval::Hour2)]
1320 #[case(BarAggregation::Minute, 240, BybitKlineInterval::Hour4)]
1321 #[case(BarAggregation::Minute, 360, BybitKlineInterval::Hour6)]
1322 #[case(BarAggregation::Minute, 720, BybitKlineInterval::Hour12)]
1323 fn test_bar_spec_to_bybit_interval_minutes(
1324 #[case] aggregation: BarAggregation,
1325 #[case] step: u64,
1326 #[case] expected: BybitKlineInterval,
1327 ) {
1328 let result = bar_spec_to_bybit_interval(aggregation, step).unwrap();
1329 assert_eq!(result, expected);
1330 }
1331
1332 #[rstest]
1333 #[case(BarAggregation::Hour, 1, BybitKlineInterval::Hour1)]
1334 #[case(BarAggregation::Hour, 2, BybitKlineInterval::Hour2)]
1335 #[case(BarAggregation::Hour, 4, BybitKlineInterval::Hour4)]
1336 #[case(BarAggregation::Hour, 6, BybitKlineInterval::Hour6)]
1337 #[case(BarAggregation::Hour, 12, BybitKlineInterval::Hour12)]
1338 fn test_bar_spec_to_bybit_interval_hours(
1339 #[case] aggregation: BarAggregation,
1340 #[case] step: u64,
1341 #[case] expected: BybitKlineInterval,
1342 ) {
1343 let result = bar_spec_to_bybit_interval(aggregation, step).unwrap();
1344 assert_eq!(result, expected);
1345 }
1346
1347 #[rstest]
1348 #[case(BarAggregation::Day, 1, BybitKlineInterval::Day1)]
1349 #[case(BarAggregation::Week, 1, BybitKlineInterval::Week1)]
1350 #[case(BarAggregation::Month, 1, BybitKlineInterval::Month1)]
1351 fn test_bar_spec_to_bybit_interval_day_week_month(
1352 #[case] aggregation: BarAggregation,
1353 #[case] step: u64,
1354 #[case] expected: BybitKlineInterval,
1355 ) {
1356 let result = bar_spec_to_bybit_interval(aggregation, step).unwrap();
1357 assert_eq!(result, expected);
1358 }
1359
1360 #[rstest]
1361 #[case(BarAggregation::Minute, 2)]
1362 #[case(BarAggregation::Minute, 10)]
1363 #[case(BarAggregation::Hour, 3)]
1364 #[case(BarAggregation::Hour, 24)]
1365 #[case(BarAggregation::Day, 2)]
1366 #[case(BarAggregation::Week, 2)]
1367 #[case(BarAggregation::Month, 2)]
1368 fn test_bar_spec_to_bybit_interval_unsupported_steps(
1369 #[case] aggregation: BarAggregation,
1370 #[case] step: u64,
1371 ) {
1372 let result = bar_spec_to_bybit_interval(aggregation, step);
1373 assert!(result.is_err());
1374 }
1375
1376 #[rstest]
1377 fn test_bar_spec_to_bybit_interval_unsupported_aggregation() {
1378 let result = bar_spec_to_bybit_interval(BarAggregation::Second, 1);
1379 assert!(result.is_err());
1380 }
1381}