1use std::convert::TryFrom;
19
20use anyhow::Context;
21use nautilus_core::{nanos::UnixNanos, uuid::UUID4};
22use nautilus_model::{
23 data::{
24 Bar, BarType, BookOrder, FundingRateUpdate, OrderBookDelta, OrderBookDeltas, QuoteTick,
25 TradeTick,
26 },
27 enums::{
28 AccountType, AggressorSide, BookAction, LiquiditySide, OrderSide, OrderStatus, OrderType,
29 PositionSideSpecified, RecordFlag, TimeInForce, TriggerType,
30 },
31 events::account::state::AccountState,
32 identifiers::{AccountId, ClientOrderId, InstrumentId, TradeId, VenueOrderId},
33 instruments::{Instrument, any::InstrumentAny},
34 reports::{FillReport, OrderStatusReport, PositionStatusReport},
35 types::{AccountBalance, Money, Price, Quantity},
36};
37use rust_decimal::Decimal;
38
39use super::messages::{
40 BybitWsAccountExecution, BybitWsAccountOrder, BybitWsAccountPosition, BybitWsAccountWallet,
41 BybitWsKline, BybitWsOrderbookDepthMsg, BybitWsTickerLinear, BybitWsTickerLinearMsg,
42 BybitWsTickerOptionMsg, BybitWsTrade,
43};
44use crate::common::{
45 enums::{
46 BybitOrderStatus, BybitOrderType, BybitStopOrderType, BybitTimeInForce,
47 BybitTriggerDirection,
48 },
49 parse::{
50 get_currency, parse_millis_timestamp, parse_price_with_precision,
51 parse_quantity_with_precision,
52 },
53};
54
55pub fn parse_topic(topic: &str) -> anyhow::Result<Vec<&str>> {
61 let parts: Vec<&str> = topic.split('.').collect();
62 if parts.is_empty() {
63 anyhow::bail!("Invalid topic format: empty topic");
64 }
65 Ok(parts)
66}
67
68pub fn parse_kline_topic(topic: &str) -> anyhow::Result<(&str, &str)> {
76 let parts = parse_topic(topic)?;
77 if parts.len() != 3 || parts[0] != "kline" {
78 anyhow::bail!(
79 "Invalid kline topic format: expected 'kline.{{interval}}.{{symbol}}', was '{topic}'"
80 );
81 }
82 Ok((parts[1], parts[2]))
83}
84
85pub fn parse_ws_trade_tick(
87 trade: &BybitWsTrade,
88 instrument: &InstrumentAny,
89 ts_init: UnixNanos,
90) -> anyhow::Result<TradeTick> {
91 let price = parse_price_with_precision(&trade.p, instrument.price_precision(), "trade.p")?;
92 let size = parse_quantity_with_precision(&trade.v, instrument.size_precision(), "trade.v")?;
93 let aggressor: AggressorSide = trade.taker_side.into();
94 let trade_id = TradeId::new_checked(trade.i.as_str())
95 .context("invalid trade identifier in Bybit trade message")?;
96 let ts_event = parse_millis_i64(trade.t, "trade.T")?;
97
98 TradeTick::new_checked(
99 instrument.id(),
100 price,
101 size,
102 aggressor,
103 trade_id,
104 ts_event,
105 ts_init,
106 )
107 .context("failed to construct TradeTick from Bybit trade message")
108}
109
110pub fn parse_orderbook_deltas(
112 msg: &BybitWsOrderbookDepthMsg,
113 instrument: &InstrumentAny,
114 ts_init: UnixNanos,
115) -> anyhow::Result<OrderBookDeltas> {
116 let is_snapshot = msg.msg_type.eq_ignore_ascii_case("snapshot");
117 let ts_event = parse_millis_i64(msg.ts, "orderbook.ts")?;
118 let ts_init = if ts_init.is_zero() { ts_event } else { ts_init };
119
120 let depth = &msg.data;
121 let instrument_id = instrument.id();
122 let price_precision = instrument.price_precision();
123 let size_precision = instrument.size_precision();
124 let update_id = u64::try_from(depth.u)
125 .context("received negative update id in Bybit order book message")?;
126 let sequence = u64::try_from(depth.seq)
127 .context("received negative sequence in Bybit order book message")?;
128
129 let mut deltas = Vec::new();
130
131 if is_snapshot {
132 deltas.push(OrderBookDelta::clear(
133 instrument_id,
134 sequence,
135 ts_event,
136 ts_init,
137 ));
138 }
139
140 let total_levels = depth.b.len() + depth.a.len();
141 let mut processed = 0_usize;
142
143 let mut push_level = |values: &[String], side: OrderSide| -> anyhow::Result<()> {
144 let (price, size) = parse_book_level(values, price_precision, size_precision, "orderbook")?;
145 let action = if size.is_zero() {
146 BookAction::Delete
147 } else if is_snapshot {
148 BookAction::Add
149 } else {
150 BookAction::Update
151 };
152
153 processed += 1;
154 let mut flags = RecordFlag::F_MBP as u8;
155 if processed == total_levels {
156 flags |= RecordFlag::F_LAST as u8;
157 }
158
159 let order = BookOrder::new(side, price, size, update_id);
160 let delta = OrderBookDelta::new_checked(
161 instrument_id,
162 action,
163 order,
164 flags,
165 sequence,
166 ts_event,
167 ts_init,
168 )
169 .context("failed to construct OrderBookDelta from Bybit book level")?;
170 deltas.push(delta);
171 Ok(())
172 };
173
174 for level in &depth.b {
175 push_level(level, OrderSide::Buy)?;
176 }
177 for level in &depth.a {
178 push_level(level, OrderSide::Sell)?;
179 }
180
181 if total_levels == 0
182 && let Some(last) = deltas.last_mut()
183 {
184 last.flags |= RecordFlag::F_LAST as u8;
185 }
186
187 OrderBookDeltas::new_checked(instrument_id, deltas)
188 .context("failed to assemble OrderBookDeltas from Bybit message")
189}
190
191pub fn parse_orderbook_quote(
193 msg: &BybitWsOrderbookDepthMsg,
194 instrument: &InstrumentAny,
195 last_quote: Option<&QuoteTick>,
196 ts_init: UnixNanos,
197) -> anyhow::Result<QuoteTick> {
198 let ts_event = parse_millis_i64(msg.ts, "orderbook.ts")?;
199 let ts_init = if ts_init.is_zero() { ts_event } else { ts_init };
200 let price_precision = instrument.price_precision();
201 let size_precision = instrument.size_precision();
202
203 let get_best =
204 |levels: &[Vec<String>], label: &str| -> anyhow::Result<Option<(Price, Quantity)>> {
205 if let Some(values) = levels.first() {
206 parse_book_level(values, price_precision, size_precision, label).map(Some)
207 } else {
208 Ok(None)
209 }
210 };
211
212 let bids = get_best(&msg.data.b, "bid")?;
213 let asks = get_best(&msg.data.a, "ask")?;
214
215 let (bid_price, bid_size) = match (bids, last_quote) {
216 (Some(level), _) => level,
217 (None, Some(prev)) => (prev.bid_price, prev.bid_size),
218 (None, None) => {
219 anyhow::bail!(
220 "Bybit order book update missing bid levels and no previous quote provided"
221 );
222 }
223 };
224
225 let (ask_price, ask_size) = match (asks, last_quote) {
226 (Some(level), _) => level,
227 (None, Some(prev)) => (prev.ask_price, prev.ask_size),
228 (None, None) => {
229 anyhow::bail!(
230 "Bybit order book update missing ask levels and no previous quote provided"
231 );
232 }
233 };
234
235 QuoteTick::new_checked(
236 instrument.id(),
237 bid_price,
238 ask_price,
239 bid_size,
240 ask_size,
241 ts_event,
242 ts_init,
243 )
244 .context("failed to construct QuoteTick from Bybit order book message")
245}
246
247pub fn parse_ticker_linear_quote(
249 msg: &BybitWsTickerLinearMsg,
250 instrument: &InstrumentAny,
251 ts_init: UnixNanos,
252) -> anyhow::Result<QuoteTick> {
253 let ts_event = parse_millis_i64(msg.ts, "ticker.ts")?;
254 let ts_init = if ts_init.is_zero() { ts_event } else { ts_init };
255 let price_precision = instrument.price_precision();
256 let size_precision = instrument.size_precision();
257
258 let data = &msg.data;
259 let bid_price = data
260 .bid1_price
261 .as_ref()
262 .context("Bybit ticker message missing bid1Price")?
263 .as_str();
264 let ask_price = data
265 .ask1_price
266 .as_ref()
267 .context("Bybit ticker message missing ask1Price")?
268 .as_str();
269
270 let bid_price = parse_price_with_precision(bid_price, price_precision, "ticker.bid1Price")?;
271 let ask_price = parse_price_with_precision(ask_price, price_precision, "ticker.ask1Price")?;
272
273 let bid_size_str = data.bid1_size.as_deref().unwrap_or("0");
274 let ask_size_str = data.ask1_size.as_deref().unwrap_or("0");
275
276 let bid_size = parse_quantity_with_precision(bid_size_str, size_precision, "ticker.bid1Size")?;
277 let ask_size = parse_quantity_with_precision(ask_size_str, size_precision, "ticker.ask1Size")?;
278
279 QuoteTick::new_checked(
280 instrument.id(),
281 bid_price,
282 ask_price,
283 bid_size,
284 ask_size,
285 ts_event,
286 ts_init,
287 )
288 .context("failed to construct QuoteTick from Bybit linear ticker message")
289}
290
291pub fn parse_ticker_option_quote(
293 msg: &BybitWsTickerOptionMsg,
294 instrument: &InstrumentAny,
295 ts_init: UnixNanos,
296) -> anyhow::Result<QuoteTick> {
297 let ts_event = parse_millis_i64(msg.ts, "ticker.ts")?;
298 let ts_init = if ts_init.is_zero() { ts_event } else { ts_init };
299 let price_precision = instrument.price_precision();
300 let size_precision = instrument.size_precision();
301
302 let data = &msg.data;
303 let bid_price =
304 parse_price_with_precision(&data.bid_price, price_precision, "ticker.bidPrice")?;
305 let ask_price =
306 parse_price_with_precision(&data.ask_price, price_precision, "ticker.askPrice")?;
307 let bid_size = parse_quantity_with_precision(&data.bid_size, size_precision, "ticker.bidSize")?;
308 let ask_size = parse_quantity_with_precision(&data.ask_size, size_precision, "ticker.askSize")?;
309
310 QuoteTick::new_checked(
311 instrument.id(),
312 bid_price,
313 ask_price,
314 bid_size,
315 ask_size,
316 ts_event,
317 ts_init,
318 )
319 .context("failed to construct QuoteTick from Bybit option ticker message")
320}
321
322pub fn parse_ticker_linear_funding(
328 data: &BybitWsTickerLinear,
329 instrument_id: InstrumentId,
330 ts_event: UnixNanos,
331 ts_init: UnixNanos,
332) -> anyhow::Result<FundingRateUpdate> {
333 let funding_rate_str = data
334 .funding_rate
335 .as_ref()
336 .context("Bybit ticker missing funding_rate")?;
337
338 let funding_rate = funding_rate_str
339 .as_str()
340 .parse::<Decimal>()
341 .context("invalid funding_rate value")?
342 .normalize();
343
344 let next_funding_ns = if let Some(next_funding_time) = &data.next_funding_time {
345 let next_funding_millis = next_funding_time
346 .as_str()
347 .parse::<i64>()
348 .context("invalid next_funding_time value")?;
349 Some(parse_millis_i64(next_funding_millis, "next_funding_time")?)
350 } else {
351 None
352 };
353
354 Ok(FundingRateUpdate::new(
355 instrument_id,
356 funding_rate,
357 next_funding_ns,
358 ts_event,
359 ts_init,
360 ))
361}
362
363pub(crate) fn parse_millis_i64(value: i64, field: &str) -> anyhow::Result<UnixNanos> {
364 if value < 0 {
365 Err(anyhow::anyhow!("{field} must be non-negative, was {value}"))
366 } else {
367 parse_millis_timestamp(&value.to_string(), field)
368 }
369}
370
371fn parse_book_level(
372 level: &[String],
373 price_precision: u8,
374 size_precision: u8,
375 label: &str,
376) -> anyhow::Result<(Price, Quantity)> {
377 let price_str = level
378 .first()
379 .ok_or_else(|| anyhow::anyhow!("missing price component in {label} level"))?;
380 let size_str = level
381 .get(1)
382 .ok_or_else(|| anyhow::anyhow!("missing size component in {label} level"))?;
383 let price = parse_price_with_precision(price_str, price_precision, label)?;
384 let size = parse_quantity_with_precision(size_str, size_precision, label)?;
385 Ok((price, size))
386}
387
388pub fn parse_ws_kline_bar(
394 kline: &BybitWsKline,
395 instrument: &InstrumentAny,
396 bar_type: BarType,
397 timestamp_on_close: bool,
398 ts_init: UnixNanos,
399) -> anyhow::Result<Bar> {
400 let price_precision = instrument.price_precision();
401 let size_precision = instrument.size_precision();
402
403 let open = parse_price_with_precision(&kline.open, price_precision, "kline.open")?;
404 let high = parse_price_with_precision(&kline.high, price_precision, "kline.high")?;
405 let low = parse_price_with_precision(&kline.low, price_precision, "kline.low")?;
406 let close = parse_price_with_precision(&kline.close, price_precision, "kline.close")?;
407 let volume = parse_quantity_with_precision(&kline.volume, size_precision, "kline.volume")?;
408
409 let mut ts_event = parse_millis_i64(kline.start, "kline.start")?;
410 if timestamp_on_close {
411 let interval_ns = bar_type
412 .spec()
413 .timedelta()
414 .num_nanoseconds()
415 .context("bar specification produced non-integer interval")?;
416 let interval_ns = u64::try_from(interval_ns)
417 .context("bar interval overflowed the u64 range for nanoseconds")?;
418 let updated = ts_event
419 .as_u64()
420 .checked_add(interval_ns)
421 .context("bar timestamp overflowed when adjusting to close time")?;
422 ts_event = UnixNanos::from(updated);
423 }
424 let ts_init = if ts_init.is_zero() { ts_event } else { ts_init };
425
426 Bar::new_checked(bar_type, open, high, low, close, volume, ts_event, ts_init)
427 .context("failed to construct Bar from Bybit WebSocket kline")
428}
429
430pub fn parse_ws_order_status_report(
436 order: &BybitWsAccountOrder,
437 instrument: &InstrumentAny,
438 account_id: AccountId,
439 ts_init: UnixNanos,
440) -> anyhow::Result<OrderStatusReport> {
441 let instrument_id = instrument.id();
442 let venue_order_id = VenueOrderId::new(order.order_id.as_str());
443 let order_side: OrderSide = order.side.into();
444
445 use crate::common::enums::BybitOrderSide;
447 let order_type: OrderType = match (
448 order.order_type,
449 order.stop_order_type,
450 order.trigger_direction,
451 order.side,
452 ) {
453 (BybitOrderType::Market, BybitStopOrderType::None | BybitStopOrderType::Unknown, _, _) => {
454 OrderType::Market
455 }
456 (BybitOrderType::Limit, BybitStopOrderType::None | BybitStopOrderType::Unknown, _, _) => {
457 OrderType::Limit
458 }
459
460 (
461 BybitOrderType::Market,
462 BybitStopOrderType::Stop,
463 BybitTriggerDirection::RisesTo,
464 BybitOrderSide::Buy,
465 ) => OrderType::StopMarket,
466 (
467 BybitOrderType::Market,
468 BybitStopOrderType::Stop,
469 BybitTriggerDirection::FallsTo,
470 BybitOrderSide::Buy,
471 ) => OrderType::MarketIfTouched,
472
473 (
474 BybitOrderType::Market,
475 BybitStopOrderType::Stop,
476 BybitTriggerDirection::FallsTo,
477 BybitOrderSide::Sell,
478 ) => OrderType::StopMarket,
479 (
480 BybitOrderType::Market,
481 BybitStopOrderType::Stop,
482 BybitTriggerDirection::RisesTo,
483 BybitOrderSide::Sell,
484 ) => OrderType::MarketIfTouched,
485
486 (
487 BybitOrderType::Limit,
488 BybitStopOrderType::Stop,
489 BybitTriggerDirection::RisesTo,
490 BybitOrderSide::Buy,
491 ) => OrderType::StopLimit,
492 (
493 BybitOrderType::Limit,
494 BybitStopOrderType::Stop,
495 BybitTriggerDirection::FallsTo,
496 BybitOrderSide::Buy,
497 ) => OrderType::LimitIfTouched,
498
499 (
500 BybitOrderType::Limit,
501 BybitStopOrderType::Stop,
502 BybitTriggerDirection::FallsTo,
503 BybitOrderSide::Sell,
504 ) => OrderType::StopLimit,
505 (
506 BybitOrderType::Limit,
507 BybitStopOrderType::Stop,
508 BybitTriggerDirection::RisesTo,
509 BybitOrderSide::Sell,
510 ) => OrderType::LimitIfTouched,
511
512 (BybitOrderType::Market, BybitStopOrderType::Stop, BybitTriggerDirection::None, _) => {
514 OrderType::Market
515 }
516 (BybitOrderType::Limit, BybitStopOrderType::Stop, BybitTriggerDirection::None, _) => {
517 OrderType::Limit
518 }
519
520 (BybitOrderType::Market, _, _, _) => OrderType::Market,
522 (BybitOrderType::Limit, _, _, _) => OrderType::Limit,
523
524 (BybitOrderType::Unknown, _, _, _) => OrderType::Limit,
525 };
526
527 let time_in_force: TimeInForce = match order.time_in_force {
528 BybitTimeInForce::Gtc => TimeInForce::Gtc,
529 BybitTimeInForce::Ioc => TimeInForce::Ioc,
530 BybitTimeInForce::Fok => TimeInForce::Fok,
531 BybitTimeInForce::PostOnly => TimeInForce::Gtc,
532 };
533
534 let quantity =
535 parse_quantity_with_precision(&order.qty, instrument.size_precision(), "order.qty")?;
536
537 let filled_qty = parse_quantity_with_precision(
538 &order.cum_exec_qty,
539 instrument.size_precision(),
540 "order.cumExecQty",
541 )?;
542
543 let order_status: OrderStatus = match order.order_status {
549 BybitOrderStatus::Created | BybitOrderStatus::New | BybitOrderStatus::Untriggered => {
550 OrderStatus::Accepted
551 }
552 BybitOrderStatus::Rejected => {
553 if filled_qty.is_positive() {
554 OrderStatus::Canceled
555 } else {
556 OrderStatus::Rejected
557 }
558 }
559 BybitOrderStatus::PartiallyFilled => OrderStatus::PartiallyFilled,
560 BybitOrderStatus::Filled => OrderStatus::Filled,
561 BybitOrderStatus::Canceled | BybitOrderStatus::PartiallyFilledCanceled => {
562 OrderStatus::Canceled
563 }
564 BybitOrderStatus::Triggered => OrderStatus::Triggered,
565 BybitOrderStatus::Deactivated => OrderStatus::Canceled,
566 };
567
568 let ts_accepted = parse_millis_timestamp(&order.created_time, "order.createdTime")?;
569 let ts_last = parse_millis_timestamp(&order.updated_time, "order.updatedTime")?;
570
571 let mut report = OrderStatusReport::new(
572 account_id,
573 instrument_id,
574 None,
575 venue_order_id,
576 order_side,
577 order_type,
578 time_in_force,
579 order_status,
580 quantity,
581 filled_qty,
582 ts_accepted,
583 ts_last,
584 ts_init,
585 Some(UUID4::new()),
586 );
587
588 if !order.order_link_id.is_empty() {
589 report = report.with_client_order_id(ClientOrderId::new(order.order_link_id.as_str()));
590 }
591
592 if !order.price.is_empty() && order.price != "0" {
593 let price =
594 parse_price_with_precision(&order.price, instrument.price_precision(), "order.price")?;
595 report = report.with_price(price);
596 }
597
598 if !order.avg_price.is_empty() && order.avg_price != "0" {
599 let avg_px = order
600 .avg_price
601 .parse::<f64>()
602 .with_context(|| format!("Failed to parse avg_price='{}' as f64", order.avg_price))?;
603 report = report.with_avg_px(avg_px)?;
604 }
605
606 if !order.trigger_price.is_empty() && order.trigger_price != "0" {
607 let trigger_price = parse_price_with_precision(
608 &order.trigger_price,
609 instrument.price_precision(),
610 "order.triggerPrice",
611 )?;
612 report = report.with_trigger_price(trigger_price);
613
614 let trigger_type: TriggerType = order.trigger_by.into();
616 report = report.with_trigger_type(trigger_type);
617 }
618
619 if order.reduce_only {
620 report = report.with_reduce_only(true);
621 }
622
623 if order.time_in_force == BybitTimeInForce::PostOnly {
624 report = report.with_post_only(true);
625 }
626
627 if !order.reject_reason.is_empty() {
628 report = report.with_cancel_reason(order.reject_reason.to_string());
629 }
630
631 Ok(report)
632}
633
634pub fn parse_ws_fill_report(
640 execution: &BybitWsAccountExecution,
641 account_id: AccountId,
642 instrument: &InstrumentAny,
643 ts_init: UnixNanos,
644) -> anyhow::Result<FillReport> {
645 let instrument_id = instrument.id();
646 let venue_order_id = VenueOrderId::new(execution.order_id.as_str());
647 let trade_id = TradeId::new_checked(execution.exec_id.as_str())
648 .context("invalid execId in Bybit WebSocket execution payload")?;
649
650 let order_side: OrderSide = execution.side.into();
651 let last_qty = parse_quantity_with_precision(
652 &execution.exec_qty,
653 instrument.size_precision(),
654 "execution.execQty",
655 )?;
656 let last_px = parse_price_with_precision(
657 &execution.exec_price,
658 instrument.price_precision(),
659 "execution.execPrice",
660 )?;
661
662 let liquidity_side = if execution.is_maker {
663 LiquiditySide::Maker
664 } else {
665 LiquiditySide::Taker
666 };
667
668 let commission_str = execution.exec_fee.trim_start_matches('-');
669 let commission_amount = commission_str
670 .parse::<f64>()
671 .with_context(|| format!("Failed to parse execFee='{}' as f64", execution.exec_fee))?
672 .abs();
673
674 let commission_currency = instrument.quote_currency();
676 let commission = Money::new(commission_amount, commission_currency);
677 let ts_event = parse_millis_timestamp(&execution.exec_time, "execution.execTime")?;
678
679 let client_order_id = if !execution.order_link_id.is_empty() {
680 Some(ClientOrderId::new(execution.order_link_id.as_str()))
681 } else {
682 None
683 };
684
685 Ok(FillReport::new(
686 account_id,
687 instrument_id,
688 venue_order_id,
689 trade_id,
690 order_side,
691 last_qty,
692 last_px,
693 commission,
694 liquidity_side,
695 client_order_id,
696 None, ts_event,
698 ts_init,
699 None, ))
701}
702
703pub fn parse_ws_position_status_report(
709 position: &BybitWsAccountPosition,
710 account_id: AccountId,
711 instrument: &InstrumentAny,
712 ts_init: UnixNanos,
713) -> anyhow::Result<PositionStatusReport> {
714 let instrument_id = instrument.id();
715
716 let quantity = parse_quantity_with_precision(
718 &position.size,
719 instrument.size_precision(),
720 "position.size",
721 )?;
722
723 let position_side = if position.side.eq_ignore_ascii_case("buy") {
725 PositionSideSpecified::Long
726 } else if position.side.eq_ignore_ascii_case("sell") {
727 PositionSideSpecified::Short
728 } else {
729 PositionSideSpecified::Flat
730 };
731
732 let ts_last = parse_millis_timestamp(&position.updated_time, "position.updatedTime")?;
733
734 Ok(PositionStatusReport::new(
735 account_id,
736 instrument_id,
737 position_side,
738 quantity,
739 ts_last,
740 ts_init,
741 None, None, position.entry_price, ))
745}
746
747pub fn parse_ws_account_state(
753 wallet: &BybitWsAccountWallet,
754 account_id: AccountId,
755 ts_event: UnixNanos,
756 ts_init: UnixNanos,
757) -> anyhow::Result<AccountState> {
758 let mut balances = Vec::new();
759
760 for coin_data in &wallet.coin {
761 let currency = get_currency(coin_data.coin.as_str());
762 let total_dec = coin_data.wallet_balance - coin_data.spot_borrow;
763 let locked_dec = coin_data.total_order_im + coin_data.total_position_im;
764
765 let total = Money::from_decimal(total_dec, currency)?;
766 let locked = Money::from_decimal(locked_dec, currency)?;
767 let free = Money::from_raw(total.raw - locked.raw, currency);
768
769 let balance = AccountBalance::new(total, locked, free);
770 balances.push(balance);
771 }
772
773 Ok(AccountState::new(
774 account_id,
775 AccountType::Margin, balances,
777 vec![], true, UUID4::new(),
780 ts_event,
781 ts_init,
782 None, ))
784}
785
786#[cfg(test)]
791mod tests {
792 use nautilus_model::{
793 data::BarSpecification,
794 enums::{AggregationSource, BarAggregation, PositionSide, PriceType},
795 };
796 use rstest::rstest;
797 use rust_decimal_macros::dec;
798
799 use super::*;
800 use crate::{
801 common::{
802 parse::{parse_linear_instrument, parse_option_instrument},
803 testing::load_test_json,
804 },
805 http::models::{BybitInstrumentLinearResponse, BybitInstrumentOptionResponse},
806 websocket::messages::{
807 BybitWsOrderbookDepthMsg, BybitWsTickerLinearMsg, BybitWsTickerOptionMsg,
808 BybitWsTradeMsg,
809 },
810 };
811
812 const TS: UnixNanos = UnixNanos::new(1_700_000_000_000_000_000);
813
814 use ustr::Ustr;
815
816 use crate::http::models::BybitFeeRate;
817
818 fn sample_fee_rate(
819 symbol: &str,
820 taker: &str,
821 maker: &str,
822 base_coin: Option<&str>,
823 ) -> BybitFeeRate {
824 BybitFeeRate {
825 symbol: Ustr::from(symbol),
826 taker_fee_rate: taker.to_string(),
827 maker_fee_rate: maker.to_string(),
828 base_coin: base_coin.map(Ustr::from),
829 }
830 }
831
832 fn linear_instrument() -> InstrumentAny {
833 let json = load_test_json("http_get_instruments_linear.json");
834 let response: BybitInstrumentLinearResponse = serde_json::from_str(&json).unwrap();
835 let instrument = &response.result.list[0];
836 let fee_rate = sample_fee_rate("BTCUSDT", "0.00055", "0.0001", Some("BTC"));
837 parse_linear_instrument(instrument, &fee_rate, TS, TS).unwrap()
838 }
839
840 fn option_instrument() -> InstrumentAny {
841 let json = load_test_json("http_get_instruments_option.json");
842 let response: BybitInstrumentOptionResponse = serde_json::from_str(&json).unwrap();
843 let instrument = &response.result.list[0];
844 parse_option_instrument(instrument, TS, TS).unwrap()
845 }
846
847 #[rstest]
848 fn parse_ws_trade_into_trade_tick() {
849 let instrument = linear_instrument();
850 let json = load_test_json("ws_public_trade.json");
851 let msg: BybitWsTradeMsg = serde_json::from_str(&json).unwrap();
852 let trade = &msg.data[0];
853
854 let tick = parse_ws_trade_tick(trade, &instrument, TS).unwrap();
855
856 assert_eq!(tick.instrument_id, instrument.id());
857 assert_eq!(tick.price, instrument.make_price(27451.00));
858 assert_eq!(tick.size, instrument.make_qty(0.010, None));
859 assert_eq!(tick.aggressor_side, AggressorSide::Buyer);
860 assert_eq!(
861 tick.trade_id.to_string(),
862 "9dc75fca-4bdd-4773-9f78-6f5d7ab2a110"
863 );
864 assert_eq!(tick.ts_event, UnixNanos::new(1_709_891_679_000_000_000));
865 }
866
867 #[rstest]
868 fn parse_orderbook_snapshot_into_deltas() {
869 let instrument = linear_instrument();
870 let json = load_test_json("ws_orderbook_snapshot.json");
871 let msg: BybitWsOrderbookDepthMsg = serde_json::from_str(&json).unwrap();
872
873 let deltas = parse_orderbook_deltas(&msg, &instrument, TS).unwrap();
874
875 assert_eq!(deltas.instrument_id, instrument.id());
876 assert_eq!(deltas.deltas.len(), 5);
877 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
878 assert_eq!(
879 deltas.deltas[1].order.price,
880 instrument.make_price(27450.00)
881 );
882 assert_eq!(
883 deltas.deltas[1].order.size,
884 instrument.make_qty(0.500, None)
885 );
886 let last = deltas.deltas.last().unwrap();
887 assert_eq!(last.order.side, OrderSide::Sell);
888 assert_eq!(last.order.price, instrument.make_price(27451.50));
889 assert_eq!(
890 last.flags & RecordFlag::F_LAST as u8,
891 RecordFlag::F_LAST as u8
892 );
893 }
894
895 #[rstest]
896 fn parse_orderbook_delta_marks_actions() {
897 let instrument = linear_instrument();
898 let json = load_test_json("ws_orderbook_delta.json");
899 let msg: BybitWsOrderbookDepthMsg = serde_json::from_str(&json).unwrap();
900
901 let deltas = parse_orderbook_deltas(&msg, &instrument, TS).unwrap();
902
903 assert_eq!(deltas.deltas.len(), 2);
904 let bid = &deltas.deltas[0];
905 assert_eq!(bid.action, BookAction::Update);
906 assert_eq!(bid.order.side, OrderSide::Buy);
907 assert_eq!(bid.order.size, instrument.make_qty(0.400, None));
908
909 let ask = &deltas.deltas[1];
910 assert_eq!(ask.action, BookAction::Delete);
911 assert_eq!(ask.order.side, OrderSide::Sell);
912 assert_eq!(ask.order.size, instrument.make_qty(0.0, None));
913 assert_eq!(
914 ask.flags & RecordFlag::F_LAST as u8,
915 RecordFlag::F_LAST as u8
916 );
917 }
918
919 #[rstest]
920 fn parse_orderbook_quote_produces_top_of_book() {
921 let instrument = linear_instrument();
922 let json = load_test_json("ws_orderbook_snapshot.json");
923 let msg: BybitWsOrderbookDepthMsg = serde_json::from_str(&json).unwrap();
924
925 let quote = parse_orderbook_quote(&msg, &instrument, None, TS).unwrap();
926
927 assert_eq!(quote.instrument_id, instrument.id());
928 assert_eq!(quote.bid_price, instrument.make_price(27450.00));
929 assert_eq!(quote.bid_size, instrument.make_qty(0.500, None));
930 assert_eq!(quote.ask_price, instrument.make_price(27451.00));
931 assert_eq!(quote.ask_size, instrument.make_qty(0.750, None));
932 }
933
934 #[rstest]
935 fn parse_orderbook_quote_with_delta_updates_sizes() {
936 let instrument = linear_instrument();
937 let snapshot: BybitWsOrderbookDepthMsg =
938 serde_json::from_str(&load_test_json("ws_orderbook_snapshot.json")).unwrap();
939 let base_quote = parse_orderbook_quote(&snapshot, &instrument, None, TS).unwrap();
940
941 let delta: BybitWsOrderbookDepthMsg =
942 serde_json::from_str(&load_test_json("ws_orderbook_delta.json")).unwrap();
943 let updated = parse_orderbook_quote(&delta, &instrument, Some(&base_quote), TS).unwrap();
944
945 assert_eq!(updated.bid_price, instrument.make_price(27450.00));
946 assert_eq!(updated.bid_size, instrument.make_qty(0.400, None));
947 assert_eq!(updated.ask_price, instrument.make_price(27451.00));
948 assert_eq!(updated.ask_size, instrument.make_qty(0.0, None));
949 }
950
951 #[rstest]
952 fn parse_linear_ticker_quote_to_quote_tick() {
953 let instrument = linear_instrument();
954 let json = load_test_json("ws_ticker_linear.json");
955 let msg: BybitWsTickerLinearMsg = serde_json::from_str(&json).unwrap();
956
957 let quote = parse_ticker_linear_quote(&msg, &instrument, TS).unwrap();
958
959 assert_eq!(quote.instrument_id, instrument.id());
960 assert_eq!(quote.bid_price, instrument.make_price(17215.50));
961 assert_eq!(quote.ask_price, instrument.make_price(17216.00));
962 assert_eq!(quote.bid_size, instrument.make_qty(84.489, None));
963 assert_eq!(quote.ask_size, instrument.make_qty(83.020, None));
964 assert_eq!(quote.ts_event, UnixNanos::new(1_673_272_861_686_000_000));
965 assert_eq!(quote.ts_init, TS);
966 }
967
968 #[rstest]
969 fn parse_option_ticker_quote_to_quote_tick() {
970 let instrument = option_instrument();
971 let json = load_test_json("ws_ticker_option.json");
972 let msg: BybitWsTickerOptionMsg = serde_json::from_str(&json).unwrap();
973
974 let quote = parse_ticker_option_quote(&msg, &instrument, TS).unwrap();
975
976 assert_eq!(quote.instrument_id, instrument.id());
977 assert_eq!(quote.bid_price, instrument.make_price(0.0));
978 assert_eq!(quote.ask_price, instrument.make_price(10.0));
979 assert_eq!(quote.bid_size, instrument.make_qty(0.0, None));
980 assert_eq!(quote.ask_size, instrument.make_qty(5.1, None));
981 assert_eq!(quote.ts_event, UnixNanos::new(1_672_917_511_074_000_000));
982 assert_eq!(quote.ts_init, TS);
983 }
984
985 #[rstest]
986 fn parse_ws_kline_into_bar() {
987 use std::num::NonZero;
988
989 let instrument = linear_instrument();
990 let json = load_test_json("ws_kline.json");
991 let msg: crate::websocket::messages::BybitWsKlineMsg = serde_json::from_str(&json).unwrap();
992 let kline = &msg.data[0];
993
994 let bar_spec = BarSpecification {
995 step: NonZero::new(5).unwrap(),
996 aggregation: BarAggregation::Minute,
997 price_type: PriceType::Last,
998 };
999 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::External);
1000
1001 let bar = parse_ws_kline_bar(kline, &instrument, bar_type, false, TS).unwrap();
1002
1003 assert_eq!(bar.bar_type, bar_type);
1004 assert_eq!(bar.open, instrument.make_price(16649.5));
1005 assert_eq!(bar.high, instrument.make_price(16677.0));
1006 assert_eq!(bar.low, instrument.make_price(16608.0));
1007 assert_eq!(bar.close, instrument.make_price(16677.0));
1008 assert_eq!(bar.volume, instrument.make_qty(2.081, None));
1009 assert_eq!(bar.ts_event, UnixNanos::new(1_672_324_800_000_000_000));
1010 assert_eq!(bar.ts_init, TS);
1011 }
1012
1013 #[rstest]
1014 fn parse_ws_order_into_order_status_report() {
1015 let instrument = linear_instrument();
1016 let json = load_test_json("ws_account_order_filled.json");
1017 let msg: crate::websocket::messages::BybitWsAccountOrderMsg =
1018 serde_json::from_str(&json).unwrap();
1019 let order = &msg.data[0];
1020 let account_id = AccountId::new("BYBIT-001");
1021
1022 let report = parse_ws_order_status_report(order, &instrument, account_id, TS).unwrap();
1023
1024 assert_eq!(report.account_id, account_id);
1025 assert_eq!(report.instrument_id, instrument.id());
1026 assert_eq!(report.order_side, OrderSide::Buy);
1027 assert_eq!(report.order_type, OrderType::Limit);
1028 assert_eq!(report.time_in_force, TimeInForce::Gtc);
1029 assert_eq!(report.order_status, OrderStatus::Filled);
1030 assert_eq!(report.quantity, instrument.make_qty(0.100, None));
1031 assert_eq!(report.filled_qty, instrument.make_qty(0.100, None));
1032 assert_eq!(report.price, Some(instrument.make_price(30000.50)));
1033 assert_eq!(report.avg_px, Some(dec!(30000.50)));
1034 assert_eq!(
1035 report.client_order_id.as_ref().unwrap().to_string(),
1036 "test-client-order-001"
1037 );
1038 assert_eq!(
1039 report.ts_accepted,
1040 UnixNanos::new(1_672_364_262_444_000_000)
1041 );
1042 assert_eq!(report.ts_last, UnixNanos::new(1_672_364_262_457_000_000));
1043 }
1044
1045 #[rstest]
1046 fn parse_ws_order_partially_filled_rejected_maps_to_canceled() {
1047 let instrument = linear_instrument();
1048 let json = load_test_json("ws_account_order_partially_filled_rejected.json");
1049 let msg: crate::websocket::messages::BybitWsAccountOrderMsg =
1050 serde_json::from_str(&json).unwrap();
1051 let order = &msg.data[0];
1052 let account_id = AccountId::new("BYBIT-001");
1053
1054 let report = parse_ws_order_status_report(order, &instrument, account_id, TS).unwrap();
1055
1056 assert_eq!(report.order_status, OrderStatus::Canceled);
1058 assert_eq!(report.filled_qty, instrument.make_qty(50.0, None));
1059 assert_eq!(
1060 report.client_order_id.as_ref().unwrap().to_string(),
1061 "O-20251001-164609-APEX-000-49"
1062 );
1063 assert_eq!(report.cancel_reason, Some("UNKNOWN".to_string()));
1064 }
1065
1066 #[rstest]
1067 fn parse_ws_execution_into_fill_report() {
1068 let instrument = linear_instrument();
1069 let json = load_test_json("ws_account_execution.json");
1070 let msg: crate::websocket::messages::BybitWsAccountExecutionMsg =
1071 serde_json::from_str(&json).unwrap();
1072 let execution = &msg.data[0];
1073 let account_id = AccountId::new("BYBIT-001");
1074
1075 let report = parse_ws_fill_report(execution, account_id, &instrument, TS).unwrap();
1076
1077 assert_eq!(report.account_id, account_id);
1078 assert_eq!(report.instrument_id, instrument.id());
1079 assert_eq!(
1080 report.venue_order_id.to_string(),
1081 "9aac161b-8ed6-450d-9cab-c5cc67c21784"
1082 );
1083 assert_eq!(
1084 report.trade_id.to_string(),
1085 "0ab1bdf7-4219-438b-b30a-32ec863018f7"
1086 );
1087 assert_eq!(report.order_side, OrderSide::Sell);
1088 assert_eq!(report.last_qty, instrument.make_qty(0.5, None));
1089 assert_eq!(report.last_px, instrument.make_price(95900.1));
1090 assert_eq!(report.commission.as_f64(), 26.3725275);
1091 assert_eq!(report.liquidity_side, LiquiditySide::Taker);
1092 assert_eq!(
1093 report.client_order_id.as_ref().unwrap().to_string(),
1094 "test-order-link-001"
1095 );
1096 assert_eq!(report.ts_event, UnixNanos::new(1_746_270_400_353_000_000));
1097 }
1098
1099 #[rstest]
1100 fn parse_ws_position_into_position_status_report() {
1101 let instrument = linear_instrument();
1102 let json = load_test_json("ws_account_position.json");
1103 let msg: crate::websocket::messages::BybitWsAccountPositionMsg =
1104 serde_json::from_str(&json).unwrap();
1105 let position = &msg.data[0];
1106 let account_id = AccountId::new("BYBIT-001");
1107
1108 let report =
1109 parse_ws_position_status_report(position, account_id, &instrument, TS).unwrap();
1110
1111 assert_eq!(report.account_id, account_id);
1112 assert_eq!(report.instrument_id, instrument.id());
1113 assert_eq!(report.position_side.as_position_side(), PositionSide::Short);
1114 assert_eq!(report.quantity, instrument.make_qty(0.01, None));
1115 assert_eq!(
1116 report.avg_px_open,
1117 Some(Decimal::try_from(3641.075).unwrap())
1118 );
1119 assert_eq!(report.ts_last, UnixNanos::new(1_762_199_125_472_000_000));
1120 assert_eq!(report.ts_init, TS);
1121 }
1122
1123 #[rstest]
1124 fn parse_ws_position_short_into_position_status_report() {
1125 let instruments_json = load_test_json("http_get_instruments_linear.json");
1127 let instruments_response: crate::http::models::BybitInstrumentLinearResponse =
1128 serde_json::from_str(&instruments_json).unwrap();
1129 let eth_def = &instruments_response.result.list[1]; let fee_rate = crate::http::models::BybitFeeRate {
1131 symbol: ustr::Ustr::from("ETHUSDT"),
1132 taker_fee_rate: "0.00055".to_string(),
1133 maker_fee_rate: "0.0001".to_string(),
1134 base_coin: Some(ustr::Ustr::from("ETH")),
1135 };
1136 let instrument =
1137 crate::common::parse::parse_linear_instrument(eth_def, &fee_rate, TS, TS).unwrap();
1138
1139 let json = load_test_json("ws_account_position_short.json");
1140 let msg: crate::websocket::messages::BybitWsAccountPositionMsg =
1141 serde_json::from_str(&json).unwrap();
1142 let position = &msg.data[0];
1143 let account_id = AccountId::new("BYBIT-001");
1144
1145 let report =
1146 parse_ws_position_status_report(position, account_id, &instrument, TS).unwrap();
1147
1148 assert_eq!(report.account_id, account_id);
1149 assert_eq!(report.instrument_id.symbol.as_str(), "ETHUSDT-LINEAR");
1150 assert_eq!(report.position_side.as_position_side(), PositionSide::Short);
1151 assert_eq!(report.quantity, instrument.make_qty(0.01, None));
1152 assert_eq!(
1153 report.avg_px_open,
1154 Some(Decimal::try_from(3641.075).unwrap())
1155 );
1156 assert_eq!(report.ts_last, UnixNanos::new(1_762_199_125_472_000_000));
1157 assert_eq!(report.ts_init, TS);
1158 }
1159
1160 #[rstest]
1161 fn parse_ws_wallet_into_account_state() {
1162 let json = load_test_json("ws_account_wallet.json");
1163 let msg: crate::websocket::messages::BybitWsAccountWalletMsg =
1164 serde_json::from_str(&json).unwrap();
1165 let wallet = &msg.data[0];
1166 let account_id = AccountId::new("BYBIT-001");
1167 let ts_event = UnixNanos::new(1_700_034_722_104_000_000);
1168
1169 let state = parse_ws_account_state(wallet, account_id, ts_event, TS).unwrap();
1170
1171 assert_eq!(state.account_id, account_id);
1172 assert_eq!(state.account_type, AccountType::Margin);
1173 assert_eq!(state.balances.len(), 2);
1174 assert!(state.is_reported);
1175
1176 let btc_balance = &state.balances[0];
1178 assert_eq!(btc_balance.currency.code.as_str(), "BTC");
1179 assert!((btc_balance.total.as_f64() - 0.00102964).abs() < 1e-8);
1180 assert!((btc_balance.free.as_f64() - 0.00092964).abs() < 1e-8);
1181 assert!((btc_balance.locked.as_f64() - 0.0001).abs() < 1e-8);
1182
1183 let usdt_balance = &state.balances[1];
1185 assert_eq!(usdt_balance.currency.code.as_str(), "USDT");
1186 assert!((usdt_balance.total.as_f64() - 9647.75537647).abs() < 1e-6);
1187 assert!((usdt_balance.free.as_f64() - 9519.89806037).abs() < 1e-6);
1188 assert!((usdt_balance.locked.as_f64() - 127.8573161).abs() < 1e-6);
1189
1190 assert_eq!(state.ts_event, ts_event);
1191 assert_eq!(state.ts_init, TS);
1192 }
1193
1194 #[rstest]
1195 fn parse_ws_wallet_with_small_order_calculates_free_correctly() {
1196 let json = load_test_json("ws_account_wallet_small_order.json");
1200 let msg: crate::websocket::messages::BybitWsAccountWalletMsg =
1201 serde_json::from_str(&json).unwrap();
1202 let wallet = &msg.data[0];
1203 let account_id = AccountId::new("BYBIT-UNIFIED");
1204 let ts_event = UnixNanos::new(1_762_960_669_000_000_000);
1205
1206 let state = parse_ws_account_state(wallet, account_id, ts_event, TS).unwrap();
1207
1208 assert_eq!(state.account_id, account_id);
1209 assert_eq!(state.balances.len(), 1);
1210
1211 let usdt_balance = &state.balances[0];
1213 assert_eq!(usdt_balance.currency.code.as_str(), "USDT");
1214
1215 assert!((usdt_balance.total.as_f64() - 51333.82543837).abs() < 1e-6);
1217
1218 assert!((usdt_balance.locked.as_f64() - 50.028).abs() < 1e-6);
1220
1221 assert!((usdt_balance.free.as_f64() - 51283.79743837).abs() < 1e-6);
1223
1224 }
1227
1228 #[rstest]
1229 fn parse_ticker_linear_into_funding_rate() {
1230 let instrument = linear_instrument();
1231 let json = load_test_json("ws_ticker_linear.json");
1232 let msg: BybitWsTickerLinearMsg = serde_json::from_str(&json).unwrap();
1233
1234 let ts_event = UnixNanos::new(1_673_272_861_686_000_000);
1235
1236 let funding =
1237 parse_ticker_linear_funding(&msg.data, instrument.id(), ts_event, TS).unwrap();
1238
1239 assert_eq!(funding.instrument_id, instrument.id());
1240 assert_eq!(funding.rate, dec!(-0.000212)); assert_eq!(
1242 funding.next_funding_ns,
1243 Some(UnixNanos::new(1_673_280_000_000_000_000))
1244 );
1245 assert_eq!(funding.ts_event, ts_event);
1246 assert_eq!(funding.ts_init, TS);
1247 }
1248
1249 #[rstest]
1250 fn parse_ws_order_stop_market_sell_preserves_type() {
1251 let instrument = linear_instrument();
1252 let json = load_test_json("ws_account_order_stop_market.json");
1253 let msg: crate::websocket::messages::BybitWsAccountOrderMsg =
1254 serde_json::from_str(&json).unwrap();
1255 let order = &msg.data[0];
1256 let account_id = AccountId::new("BYBIT-001");
1257
1258 let report = parse_ws_order_status_report(order, &instrument, account_id, TS).unwrap();
1259
1260 assert_eq!(report.order_type, OrderType::StopMarket);
1262 assert_eq!(report.order_side, OrderSide::Sell);
1263 assert_eq!(report.order_status, OrderStatus::Accepted); assert_eq!(report.trigger_price, Some(instrument.make_price(45000.00)));
1265 assert_eq!(report.trigger_type, Some(TriggerType::LastPrice));
1266 assert_eq!(
1267 report.client_order_id.as_ref().unwrap().to_string(),
1268 "test-client-stop-market-001"
1269 );
1270 }
1271
1272 #[rstest]
1273 fn parse_ws_order_stop_market_buy_preserves_type() {
1274 let instrument = linear_instrument();
1275 let json = load_test_json("ws_account_order_buy_stop_market.json");
1276 let msg: crate::websocket::messages::BybitWsAccountOrderMsg =
1277 serde_json::from_str(&json).unwrap();
1278 let order = &msg.data[0];
1279 let account_id = AccountId::new("BYBIT-001");
1280
1281 let report = parse_ws_order_status_report(order, &instrument, account_id, TS).unwrap();
1282
1283 assert_eq!(report.order_type, OrderType::StopMarket);
1285 assert_eq!(report.order_side, OrderSide::Buy);
1286 assert_eq!(report.order_status, OrderStatus::Accepted);
1287 assert_eq!(report.trigger_price, Some(instrument.make_price(55000.00)));
1288 assert_eq!(report.trigger_type, Some(TriggerType::LastPrice));
1289 assert_eq!(
1290 report.client_order_id.as_ref().unwrap().to_string(),
1291 "test-client-buy-stop-market-001"
1292 );
1293 }
1294
1295 #[rstest]
1296 fn parse_ws_order_market_if_touched_buy_preserves_type() {
1297 let instrument = linear_instrument();
1298 let json = load_test_json("ws_account_order_market_if_touched.json");
1299 let msg: crate::websocket::messages::BybitWsAccountOrderMsg =
1300 serde_json::from_str(&json).unwrap();
1301 let order = &msg.data[0];
1302 let account_id = AccountId::new("BYBIT-001");
1303
1304 let report = parse_ws_order_status_report(order, &instrument, account_id, TS).unwrap();
1305
1306 assert_eq!(report.order_type, OrderType::MarketIfTouched);
1308 assert_eq!(report.order_side, OrderSide::Buy);
1309 assert_eq!(report.order_status, OrderStatus::Accepted); assert_eq!(report.trigger_price, Some(instrument.make_price(55000.00)));
1311 assert_eq!(report.trigger_type, Some(TriggerType::LastPrice));
1312 assert_eq!(
1313 report.client_order_id.as_ref().unwrap().to_string(),
1314 "test-client-mit-001"
1315 );
1316 }
1317
1318 #[rstest]
1319 fn parse_ws_order_market_if_touched_sell_preserves_type() {
1320 let instrument = linear_instrument();
1321 let json = load_test_json("ws_account_order_sell_market_if_touched.json");
1322 let msg: crate::websocket::messages::BybitWsAccountOrderMsg =
1323 serde_json::from_str(&json).unwrap();
1324 let order = &msg.data[0];
1325 let account_id = AccountId::new("BYBIT-001");
1326
1327 let report = parse_ws_order_status_report(order, &instrument, account_id, TS).unwrap();
1328
1329 assert_eq!(report.order_type, OrderType::MarketIfTouched);
1331 assert_eq!(report.order_side, OrderSide::Sell);
1332 assert_eq!(report.order_status, OrderStatus::Accepted);
1333 assert_eq!(report.trigger_price, Some(instrument.make_price(55000.00)));
1334 assert_eq!(
1335 report.client_order_id.as_ref().unwrap().to_string(),
1336 "test-client-sell-mit-001"
1337 );
1338 }
1339
1340 #[rstest]
1341 fn parse_ws_order_stop_limit_preserves_type() {
1342 let instrument = linear_instrument();
1343 let json = load_test_json("ws_account_order_stop_limit.json");
1344 let msg: crate::websocket::messages::BybitWsAccountOrderMsg =
1345 serde_json::from_str(&json).unwrap();
1346 let order = &msg.data[0];
1347 let account_id = AccountId::new("BYBIT-001");
1348
1349 let report = parse_ws_order_status_report(order, &instrument, account_id, TS).unwrap();
1350
1351 assert_eq!(report.order_type, OrderType::StopLimit);
1354 assert_eq!(report.order_side, OrderSide::Sell);
1355 assert_eq!(report.order_status, OrderStatus::Accepted); assert_eq!(report.price, Some(instrument.make_price(44500.00)));
1357 assert_eq!(report.trigger_price, Some(instrument.make_price(45000.00)));
1358 assert_eq!(
1359 report.client_order_id.as_ref().unwrap().to_string(),
1360 "test-client-stop-limit-001"
1361 );
1362 }
1363
1364 #[rstest]
1365 fn parse_ws_order_limit_if_touched_preserves_type() {
1366 let instrument = linear_instrument();
1367 let json = load_test_json("ws_account_order_limit_if_touched.json");
1368 let msg: crate::websocket::messages::BybitWsAccountOrderMsg =
1369 serde_json::from_str(&json).unwrap();
1370 let order = &msg.data[0];
1371 let account_id = AccountId::new("BYBIT-001");
1372
1373 let report = parse_ws_order_status_report(order, &instrument, account_id, TS).unwrap();
1374
1375 assert_eq!(report.order_type, OrderType::LimitIfTouched);
1378 assert_eq!(report.order_side, OrderSide::Buy);
1379 assert_eq!(report.order_status, OrderStatus::Accepted); assert_eq!(report.price, Some(instrument.make_price(55500.00)));
1381 assert_eq!(report.trigger_price, Some(instrument.make_price(55000.00)));
1382 assert_eq!(
1383 report.client_order_id.as_ref().unwrap().to_string(),
1384 "test-client-lit-001"
1385 );
1386 }
1387}