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