1use std::{
19 collections::VecDeque,
20 sync::{
21 Arc,
22 atomic::{AtomicBool, AtomicU64, Ordering},
23 },
24};
25
26use dashmap::DashMap;
27use nautilus_network::{
28 error::SendError,
29 retry::{RetryManager, create_websocket_retry_manager},
30 websocket::{AuthTracker, SubscriptionState, WebSocketClient},
31};
32use serde::Serialize;
33use serde_json::Value;
34use tokio_tungstenite::tungstenite::Message;
35use ustr::Ustr;
36
37use super::{
38 enums::BybitWsOperation,
39 error::{BybitWsError, create_bybit_timeout_error, should_retry_bybit_error},
40 messages::{
41 BybitWebSocketError, BybitWsFrame, BybitWsMessage, BybitWsOrderResponse, BybitWsResponse,
42 BybitWsSubscriptionMsg,
43 },
44 parse::parse_bybit_ws_frame,
45};
46use crate::common::{
47 enums::{BybitProductType, BybitWsOrderRequestOp},
48 rate_limit::{
49 BYBIT_RATE_LIMIT_HEADER, BYBIT_RATE_LIMIT_RESET_HEADER, BYBIT_RATE_LIMIT_STATUS_HEADER,
50 BybitRateLimiter,
51 },
52};
53
54#[derive(Debug)]
56pub struct BybitWsOrderCommand {
57 pub(crate) req_id: String,
58 pub(crate) op: BybitWsOrderRequestOp,
59 pub(crate) category: BybitProductType,
60 pub(crate) weight: u32,
61 pub(crate) referer: Option<String>,
62 pub(crate) args: Vec<Value>,
63}
64
65#[derive(Debug, Serialize)]
66#[serde(rename_all = "camelCase")]
67struct BybitWsSignedOrderRequest {
68 req_id: String,
69 op: BybitWsOrderRequestOp,
70 header: BybitWsSignedOrderHeader,
71 args: Vec<Value>,
72}
73
74#[derive(Debug, Serialize)]
75#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
76struct BybitWsSignedOrderHeader {
77 x_bapi_timestamp: String,
78 x_bapi_recv_window: String,
79 #[serde(rename = "Referer", skip_serializing_if = "Option::is_none")]
80 referer: Option<String>,
81}
82
83#[derive(Clone, Copy, Debug)]
84struct PendingRate {
85 endpoint: &'static str,
86 category: BybitProductType,
87}
88
89#[derive(Debug)]
90enum OrderSendFailure {
91 NotSent(BybitWsError),
92 Ambiguous(BybitWsError),
93}
94
95#[derive(Debug)]
97pub enum HandlerCommand {
98 SetClient(WebSocketClient),
99 Disconnect,
100 Authenticate { payload: String },
101 Subscribe { topics: Vec<String> },
102 Unsubscribe { topics: Vec<String> },
103 SendOrder { command: BybitWsOrderCommand },
104 SendOrders { commands: Vec<BybitWsOrderCommand> },
105}
106
107pub(super) struct BybitWsFeedHandler {
108 signal: Arc<AtomicBool>,
109 inner: Option<WebSocketClient>,
110 cmd_rx: tokio::sync::mpsc::UnboundedReceiver<HandlerCommand>,
111 raw_rx: tokio::sync::mpsc::UnboundedReceiver<Message>,
112 auth_tracker: AuthTracker,
113 subscriptions: SubscriptionState,
114 rate_limiter: BybitRateLimiter,
115 recv_window_ms: Arc<AtomicU64>,
116 pending_rates: DashMap<String, PendingRate>,
117 pending_orders: VecDeque<BybitWsOrderCommand>,
118 retry_manager: RetryManager<BybitWsError>,
119}
120
121impl BybitWsFeedHandler {
122 pub(super) fn new(
124 signal: Arc<AtomicBool>,
125 cmd_rx: tokio::sync::mpsc::UnboundedReceiver<HandlerCommand>,
126 raw_rx: tokio::sync::mpsc::UnboundedReceiver<Message>,
127 auth_tracker: AuthTracker,
128 subscriptions: SubscriptionState,
129 rate_limiter: BybitRateLimiter,
130 recv_window_ms: Arc<AtomicU64>,
131 ) -> Self {
132 Self {
133 signal,
134 inner: None,
135 cmd_rx,
136 raw_rx,
137 auth_tracker,
138 subscriptions,
139 rate_limiter,
140 recv_window_ms,
141 pending_rates: DashMap::new(),
142 pending_orders: VecDeque::new(),
143 retry_manager: create_websocket_retry_manager(),
144 }
145 }
146
147 pub(super) fn is_stopped(&self) -> bool {
148 self.signal.load(Ordering::Relaxed)
149 }
150
151 async fn send_with_retry(&self, payload: String) -> Result<(), BybitWsError> {
153 if let Some(client) = &self.inner {
154 self.retry_manager
155 .execute_with_retry(
156 "websocket_send",
157 || {
158 let payload = payload.clone();
159 async move {
160 client
161 .send_text(payload, None)
162 .await
163 .map_err(|e| BybitWsError::Transport(format!("Send failed: {e}")))
164 }
165 },
166 should_retry_bybit_error,
167 |e| create_bybit_timeout_error(e.to_string()),
168 )
169 .await
170 } else {
171 Err(BybitWsError::ClientError(
172 "No active WebSocket client".to_string(),
173 ))
174 }
175 }
176
177 async fn send_order(&self, command: &BybitWsOrderCommand) -> Result<bool, OrderSendFailure> {
178 if !self.auth_tracker.is_authenticated() {
179 return Ok(false);
180 }
181
182 let client = self.inner.as_ref().ok_or_else(|| {
183 OrderSendFailure::NotSent(BybitWsError::ClientError(
184 "No active WebSocket client".to_string(),
185 ))
186 })?;
187 let (endpoint, _) = order_operation(command.op);
188
189 self.rate_limiter
190 .acquire_ws_order(endpoint, command.category, command.weight)
191 .await
192 .map_err(|e| OrderSendFailure::NotSent(BybitWsError::ClientError(e)))?;
193
194 if !self.auth_tracker.is_authenticated() {
195 return Ok(false);
196 }
197
198 let connection_epoch = client.connection_epoch();
199 let recv_window_ms = self.recv_window_ms.load(Ordering::Acquire);
200 let request = BybitWsSignedOrderRequest {
201 req_id: command.req_id.clone(),
202 op: command.op,
203 header: BybitWsSignedOrderHeader {
204 x_bapi_timestamp: nautilus_core::time::get_atomic_clock_realtime()
205 .get_time_ms()
206 .to_string(),
207 x_bapi_recv_window: recv_window_ms.to_string(),
208 referer: command.referer.clone(),
209 },
210 args: command.args.clone(),
211 };
212 let payload = serde_json::to_string(&request)
213 .map_err(|e| OrderSendFailure::NotSent(BybitWsError::Json(e.to_string())))?;
214 self.pending_rates.insert(
215 command.req_id.clone(),
216 PendingRate {
217 endpoint,
218 category: command.category,
219 },
220 );
221
222 match client
223 .send_text_on_connection(payload, None, connection_epoch)
224 .await
225 {
226 Ok(()) => Ok(true),
227 Err(e) => self.classify_order_send_error(&command.req_id, e),
228 }
229 }
230
231 fn classify_order_send_error(
232 &self,
233 req_id: &str,
234 error: SendError,
235 ) -> Result<bool, OrderSendFailure> {
236 match error {
237 SendError::ConnectionChanged => {
238 self.pending_rates.remove(req_id);
239 self.auth_tracker.invalidate();
240 Ok(false)
241 }
242 SendError::Timeout => {
243 self.pending_rates.remove(req_id);
244 self.auth_tracker.invalidate();
245 Err(OrderSendFailure::NotSent(BybitWsError::ClientError(
246 "Order command was not written".to_string(),
247 )))
248 }
249 SendError::InvalidInput(_) | SendError::Closed => {
250 self.pending_rates.remove(req_id);
251 Err(OrderSendFailure::NotSent(BybitWsError::ClientError(
252 "Order command was not written".to_string(),
253 )))
254 }
255 error => Err(OrderSendFailure::Ambiguous(BybitWsError::Send(
256 error.to_string(),
257 ))),
258 }
259 }
260
261 pub(super) async fn next(&mut self) -> Option<BybitWsMessage> {
262 loop {
263 if self.auth_tracker.is_authenticated()
264 && let Some(command) = self.pending_orders.pop_front()
265 {
266 let req_id = command.req_id.clone();
267 match self.send_order(&command).await {
268 Ok(true) => {}
269 Ok(false) => self.pending_orders.push_front(command),
270 Err(OrderSendFailure::NotSent(error)) => {
271 return Some(order_not_sent_message(&command, &error));
272 }
273 Err(OrderSendFailure::Ambiguous(error)) => {
274 log::error!("Ambiguous order send failure: req_id={req_id}, error={error}");
275 }
276 }
277 continue;
278 }
279
280 tokio::select! {
281 Some(cmd) = self.cmd_rx.recv() => {
282 match cmd {
283 HandlerCommand::SetClient(client) => {
284 log::debug!("WebSocketClient received by handler");
285 self.inner = Some(client);
286 }
287 HandlerCommand::Disconnect => {
288 log::debug!("Disconnect command received");
289
290 if let Some(client) = self.inner.take() {
291 client.disconnect().await;
292 }
293 }
294 HandlerCommand::Authenticate { payload } => {
295 log::debug!("Authenticate command received");
296
297 if let Err(e) = self.send_with_retry(payload).await {
298 log::error!("Failed to send authentication after retries: {e}");
299 }
300 }
301 HandlerCommand::Subscribe { topics } => {
302 for topic in topics {
303 log::debug!("Subscribing to topic: topic={topic}");
304 if let Err(e) = self.send_with_retry(topic.clone()).await {
305 log::error!("Failed to send subscription after retries: topic={topic}, error={e}");
306 }
307 }
308 }
309 HandlerCommand::Unsubscribe { topics } => {
310 for topic in topics {
311 log::debug!("Unsubscribing from topic: topic={topic}");
312 if let Err(e) = self.send_with_retry(topic.clone()).await {
313 log::error!("Failed to send unsubscription after retries: topic={topic}, error={e}");
314 }
315 }
316 }
317 HandlerCommand::SendOrder { command } => {
318 let req_id = command.req_id.clone();
319 match self.send_order(&command).await {
320 Ok(true) => {}
321 Ok(false) => self.pending_orders.push_back(command),
322 Err(OrderSendFailure::NotSent(error)) => {
323 return Some(order_not_sent_message(&command, &error));
324 }
325 Err(OrderSendFailure::Ambiguous(error)) => {
326 log::error!(
327 "Ambiguous order send failure: req_id={req_id}, error={error}"
328 );
329 }
330 }
331 }
332 HandlerCommand::SendOrders { commands } => {
333 self.pending_orders.extend(commands);
334 }
335 }
336 }
337
338 () = tokio::time::sleep(std::time::Duration::from_millis(100)) => {
339 if self.signal.load(Ordering::Relaxed) {
340 log::debug!("Stop signal received during idle period");
341 return None;
342 }
343 }
344
345 msg = self.raw_rx.recv() => {
346 let msg = match msg {
347 Some(msg) => msg,
348 None => {
349 log::debug!("WebSocket stream closed");
350 return None;
351 }
352 };
353
354 if let Message::Ping(data) = &msg {
355 log::trace!("Received ping frame with {} bytes", data.len());
356
357 if let Some(client) = &self.inner
358 && let Err(e) = client.send_pong(data.to_vec()).await
359 {
360 log::warn!("Failed to send pong frame: error={e}");
361 }
362 continue;
363 }
364
365 let frame = match Self::parse_raw_frame(msg) {
366 Some(frame) => frame,
367 None => continue,
368 };
369
370 if self.signal.load(Ordering::Relaxed) {
371 log::debug!("Stop signal received");
372 return None;
373 }
374
375 match frame {
376 BybitWsFrame::Subscription(ref sub_msg) => {
377 self.handle_subscription_ack(sub_msg);
378 }
379 BybitWsFrame::Auth(auth_response) => {
380 let is_success = auth_response.success.unwrap_or(false)
381 || (auth_response.ret_code == Some(0));
382
383 if is_success {
384 self.auth_tracker.succeed();
385 log::debug!("WebSocket authenticated");
386 } else {
387 let error_msg = auth_response
388 .ret_msg
389 .as_deref()
390 .unwrap_or("Authentication rejected");
391 self.auth_tracker.fail(error_msg);
392 log::error!("WebSocket authentication failed: error={error_msg}");
393 }
394 return Some(BybitWsMessage::Auth(auth_response));
395 }
396 BybitWsFrame::ErrorResponse(ref resp) => {
397 if let Some(op) = &resp.op {
402 if *op == BybitWsOperation::Subscribe
403 || *op == BybitWsOperation::Unsubscribe
404 {
405 self.handle_subscription_error(resp);
406 } else {
407 let error = BybitWebSocketError::from_response(resp);
408 return Some(BybitWsMessage::Error(error));
409 }
410 } else {
411 let error = BybitWebSocketError::from_response(resp);
412 return Some(BybitWsMessage::Error(error));
413 }
414 }
415 BybitWsFrame::OrderResponse(resp) => {
416 self.observe_order_rate_limit(&resp);
417 return Some(BybitWsMessage::OrderResponse(resp));
418 }
419 BybitWsFrame::Orderbook(msg) => {
420 return Some(BybitWsMessage::Orderbook(msg));
421 }
422 BybitWsFrame::Trade(msg) => {
423 return Some(BybitWsMessage::Trade(msg));
424 }
425 BybitWsFrame::Kline(msg) => {
426 return Some(BybitWsMessage::Kline(msg));
427 }
428 BybitWsFrame::TickerLinear(msg) => {
429 return Some(BybitWsMessage::TickerLinear(msg));
430 }
431 BybitWsFrame::TickerOption(msg) => {
432 return Some(BybitWsMessage::TickerOption(msg));
433 }
434 BybitWsFrame::AccountOrder(msg) => {
435 return Some(BybitWsMessage::AccountOrder(msg));
436 }
437 BybitWsFrame::AccountExecution(msg) => {
438 return Some(BybitWsMessage::AccountExecution(msg));
439 }
440 BybitWsFrame::AccountExecutionFast(msg) => {
441 return Some(BybitWsMessage::AccountExecutionFast(msg));
442 }
443 BybitWsFrame::AccountWallet(msg) => {
444 return Some(BybitWsMessage::AccountWallet(msg));
445 }
446 BybitWsFrame::AccountPosition(msg) => {
447 return Some(BybitWsMessage::AccountPosition(msg));
448 }
449 BybitWsFrame::Reconnected => {
450 self.auth_tracker.invalidate();
451 return Some(BybitWsMessage::Reconnected);
452 }
453 BybitWsFrame::Unknown(value) => {
454 log::debug!("Unknown WebSocket frame: {value}");
455 }
456 }
457 }
458 }
459 }
460 }
461
462 fn observe_order_rate_limit(&self, response: &super::messages::BybitWsOrderResponse) {
463 let Some(req_id) = response.req_id.as_deref() else {
464 return;
465 };
466 let Some((_, pending)) = self.pending_rates.remove(req_id) else {
467 return;
468 };
469 let Some(header) = response.header.as_ref() else {
470 return;
471 };
472 let parse_u32 = |key: &str| {
473 header.get(key).and_then(|value| {
474 value
475 .as_u64()
476 .and_then(|value| u32::try_from(value).ok())
477 .or_else(|| value.as_str()?.parse::<u32>().ok())
478 })
479 };
480 let Some(limit) = parse_u32(BYBIT_RATE_LIMIT_HEADER) else {
481 return;
482 };
483 let Some(remaining) = parse_u32(BYBIT_RATE_LIMIT_STATUS_HEADER) else {
484 return;
485 };
486 let reset_timestamp_ms = header.get(BYBIT_RATE_LIMIT_RESET_HEADER).and_then(|value| {
487 value
488 .as_i64()
489 .or_else(|| value.as_str()?.parse::<i64>().ok())
490 });
491 self.rate_limiter.observe_account(
492 pending.endpoint,
493 Some(pending.category),
494 limit,
495 remaining,
496 reset_timestamp_ms,
497 );
498 }
499
500 fn handle_subscription_ack(&self, sub_msg: &BybitWsSubscriptionMsg) {
501 match sub_msg.op {
502 BybitWsOperation::Subscribe => {
503 if sub_msg.success {
504 if let Some(topic) = &sub_msg.req_id {
505 self.subscriptions.confirm_subscribe(topic);
506 log::debug!("Subscription confirmed: topic={topic}");
507 } else {
508 for topic in self.subscriptions.pending_subscribe_topics() {
510 self.subscriptions.confirm_subscribe(&topic);
511 log::debug!("Subscription confirmed (bulk): topic={topic}");
512 }
513 }
514 } else if let Some(topic) = &sub_msg.req_id {
515 self.subscriptions.mark_failure(topic);
516 log::warn!(
517 "Subscription failed: topic={topic}, error={:?}",
518 sub_msg.ret_msg
519 );
520 } else {
521 for topic in self.subscriptions.pending_subscribe_topics() {
522 self.subscriptions.mark_failure(&topic);
523 log::warn!(
524 "Subscription failed (bulk): topic={topic}, error={:?}",
525 sub_msg.ret_msg
526 );
527 }
528 }
529 }
530 BybitWsOperation::Unsubscribe => {
531 if sub_msg.success {
532 if let Some(topic) = &sub_msg.req_id {
533 self.subscriptions.confirm_unsubscribe(topic);
534 log::debug!("Unsubscription confirmed: topic={topic}");
535 } else {
536 for topic in self.subscriptions.pending_unsubscribe_topics() {
537 self.subscriptions.confirm_unsubscribe(&topic);
538 log::debug!("Unsubscription confirmed (bulk): topic={topic}");
539 }
540 }
541 } else {
542 let topic_desc = sub_msg.req_id.as_deref().unwrap_or("unknown");
543 log::warn!(
544 "Unsubscription failed: topic={topic_desc}, error={:?}",
545 sub_msg.ret_msg
546 );
547 }
548 }
549 _ => {}
550 }
551 }
552
553 fn handle_subscription_error(&self, resp: &BybitWsResponse) {
554 let topic = resp.req_id.as_deref().unwrap_or("unknown");
555 let error_msg = resp.ret_msg.as_deref().unwrap_or("unknown error");
556
557 match resp.op {
558 Some(BybitWsOperation::Subscribe) => {
559 if is_already_subscribed_error(error_msg)
562 && let Some(ref req_id) = resp.req_id
563 {
564 self.subscriptions.confirm_subscribe(req_id);
565 log::debug!("Subscription duplicate ignored: topic={topic}, error={error_msg}");
566 return;
567 }
568
569 if let Some(ref req_id) = resp.req_id {
570 self.subscriptions.mark_failure(req_id);
571 } else {
572 for t in self.subscriptions.pending_subscribe_topics() {
573 self.subscriptions.mark_failure(&t);
574 }
575 }
576 log::warn!("Subscription error: topic={topic}, error={error_msg}");
577 }
578 Some(BybitWsOperation::Unsubscribe) => {
579 log::warn!("Unsubscription error: topic={topic}, error={error_msg}");
580 }
581 _ => {}
582 }
583 }
584
585 fn parse_raw_frame(msg: Message) -> Option<BybitWsFrame> {
586 match msg {
587 Message::Text(text) => {
588 if text == nautilus_network::RECONNECTED {
589 log::debug!("Received WebSocket reconnected signal");
590 return Some(BybitWsFrame::Reconnected);
591 }
592
593 if text.trim().eq_ignore_ascii_case("pong") {
594 return None;
595 }
596
597 log::trace!("Raw websocket message: {text}");
598
599 let value: serde_json::Value = match serde_json::from_str(&text) {
600 Ok(v) => v,
601 Err(e) => {
602 log::error!("Failed to parse WebSocket message: {e}: {text}");
603 return None;
604 }
605 };
606
607 if value
608 .get("op")
609 .and_then(serde_json::Value::as_str)
610 .is_some_and(|op| op == BybitWsOperation::Pong.as_ref())
611 {
612 return None;
613 }
614
615 Some(parse_bybit_ws_frame(value))
616 }
617 Message::Binary(msg) => {
618 log::debug!("Raw binary frame ({} bytes)", msg.len());
619 log::trace!("Raw binary: {msg:?}");
620 None
621 }
622 Message::Close(_) => {
623 log::debug!("Received close message, waiting for reconnection");
624 None
625 }
626 _ => None,
627 }
628 }
629}
630
631fn order_operation(op: BybitWsOrderRequestOp) -> (&'static str, &'static str) {
632 match op {
633 BybitWsOrderRequestOp::Create => ("/v5/order/create", "order.create"),
634 BybitWsOrderRequestOp::Amend => ("/v5/order/amend", "order.amend"),
635 BybitWsOrderRequestOp::Cancel => ("/v5/order/cancel", "order.cancel"),
636 BybitWsOrderRequestOp::CreateBatch => ("/v5/order/create-batch", "order.create-batch"),
637 BybitWsOrderRequestOp::AmendBatch => ("/v5/order/amend-batch", "order.amend-batch"),
638 BybitWsOrderRequestOp::CancelBatch => ("/v5/order/cancel-batch", "order.cancel-batch"),
639 }
640}
641
642fn order_not_sent_message(command: &BybitWsOrderCommand, error: &BybitWsError) -> BybitWsMessage {
643 let (_, op) = order_operation(command.op);
644 BybitWsMessage::OrderResponse(BybitWsOrderResponse {
645 op: Ustr::from(op),
646 conn_id: None,
647 ret_code: -1,
648 ret_msg: error.to_string(),
649 data: Value::Object(serde_json::Map::new()),
650 req_id: Some(command.req_id.clone()),
651 header: None,
652 ret_ext_info: None,
653 })
654}
655
656fn is_already_subscribed_error(error_msg: &str) -> bool {
657 error_msg
658 .to_ascii_lowercase()
659 .contains("already subscribed")
660}
661
662#[cfg(test)]
663mod tests {
664 use rstest::rstest;
665 use ustr::Ustr;
666
667 use super::*;
668 use crate::common::{
669 consts::BYBIT_WS_TOPIC_DELIMITER, rate_limit::BybitRateLimiter, testing::load_test_json,
670 };
671
672 fn create_test_handler() -> BybitWsFeedHandler {
673 let signal = Arc::new(AtomicBool::new(false));
674 let (_cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel();
675 let (_raw_tx, raw_rx) = tokio::sync::mpsc::unbounded_channel();
676 let auth_tracker = AuthTracker::new();
677 let subscriptions = SubscriptionState::new(BYBIT_WS_TOPIC_DELIMITER);
678
679 BybitWsFeedHandler::new(
680 signal,
681 cmd_rx,
682 raw_rx,
683 auth_tracker,
684 subscriptions,
685 BybitRateLimiter::for_websocket(
686 "wss://bybit-handler-test.invalid",
687 Some("test-key"),
688 None,
689 ),
690 Arc::new(AtomicU64::new(5_000)),
691 )
692 }
693
694 fn load_value(fixture: &str) -> serde_json::Value {
695 let json = load_test_json(fixture);
696 serde_json::from_str(&json).unwrap()
697 }
698
699 #[rstest]
700 fn test_handler_initializes() {
701 let _handler = create_test_handler();
702 }
703
704 #[tokio::test]
705 async fn order_without_writer_produces_correlated_terminal_response() {
706 let handler = create_test_handler();
707 handler.auth_tracker.succeed();
708 let command = BybitWsOrderCommand {
709 req_id: "not-sent-request".to_string(),
710 op: BybitWsOrderRequestOp::Create,
711 category: BybitProductType::Linear,
712 weight: 1,
713 referer: None,
714 args: vec![serde_json::json!({"category": "linear"})],
715 };
716
717 let failure = handler.send_order(&command).await.unwrap_err();
718 let OrderSendFailure::NotSent(error) = failure else {
719 panic!("Expected definitive not-sent failure, was {failure:?}");
720 };
721 let BybitWsMessage::OrderResponse(response) = order_not_sent_message(&command, &error)
722 else {
723 panic!("Expected order response");
724 };
725
726 assert_eq!(response.op.as_str(), "order.create");
727 assert_eq!(response.ret_code, -1);
728 assert_eq!(response.req_id.as_deref(), Some("not-sent-request"));
729 assert_eq!(response.ret_msg, "Client error: No active WebSocket client");
730 }
731
732 #[rstest]
733 fn connection_change_invalidates_authentication_before_requeue() {
734 let handler = create_test_handler();
735 handler.auth_tracker.succeed();
736 handler.pending_rates.insert(
737 "reconnect-request".to_string(),
738 PendingRate {
739 endpoint: "/v5/order/create",
740 category: BybitProductType::Linear,
741 },
742 );
743
744 let result = handler
745 .classify_order_send_error("reconnect-request", SendError::ConnectionChanged)
746 .unwrap();
747
748 assert!(!result);
749 assert!(!handler.auth_tracker.is_authenticated());
750 assert!(!handler.pending_rates.contains_key("reconnect-request"));
751 }
752
753 #[rstest]
754 fn pre_active_timeout_is_terminal_not_sent() {
755 let handler = create_test_handler();
756 handler.auth_tracker.succeed();
757 handler.pending_rates.insert(
758 "timeout-request".to_string(),
759 PendingRate {
760 endpoint: "/v5/order/create",
761 category: BybitProductType::Linear,
762 },
763 );
764
765 let failure = handler
766 .classify_order_send_error("timeout-request", SendError::Timeout)
767 .unwrap_err();
768
769 assert!(matches!(failure, OrderSendFailure::NotSent(_)));
770 assert!(!handler.auth_tracker.is_authenticated());
771 assert!(!handler.pending_rates.contains_key("timeout-request"));
772 }
773
774 #[rstest]
775 fn test_parse_frame_auth_success() {
776 let value = load_value("ws_auth_success.json");
777 let frame = parse_bybit_ws_frame(value);
778 match frame {
779 BybitWsFrame::Auth(auth) => {
780 assert_eq!(auth.conn_id.as_deref(), Some("cejreaspqfm9se7usbrg-2xh"));
781 assert_eq!(auth.ret_code, Some(0));
782 assert_eq!(auth.success, Some(true));
783 }
784 other => panic!("Expected Auth, was {other:?}"),
785 }
786 }
787
788 #[rstest]
789 fn test_parse_frame_auth_failure() {
790 let value = load_value("ws_auth_failure.json");
791 let frame = parse_bybit_ws_frame(value);
792 match frame {
793 BybitWsFrame::ErrorResponse(resp) => {
794 assert_eq!(resp.ret_code, Some(10003));
795 assert_eq!(resp.ret_msg.as_deref(), Some("Invalid apikey"));
796 }
797 other => panic!("Expected ErrorResponse, was {other:?}"),
798 }
799 }
800
801 #[rstest]
802 fn test_parse_frame_subscription_ack() {
803 let value = load_value("ws_subscription_ack.json");
804 let frame = parse_bybit_ws_frame(value);
805 match frame {
806 BybitWsFrame::Subscription(sub) => {
807 assert!(sub.success);
808 assert_eq!(sub.op, BybitWsOperation::Subscribe);
809 assert_eq!(sub.req_id.as_deref(), Some("sub-orderbook-1"));
810 }
811 other => panic!("Expected Subscription, was {other:?}"),
812 }
813 }
814
815 #[rstest]
816 fn test_parse_frame_subscription_failure() {
817 let value = load_value("ws_subscription_failure.json");
818 let frame = parse_bybit_ws_frame(value);
819 match frame {
820 BybitWsFrame::ErrorResponse(resp) => {
821 assert_eq!(
822 resp.ret_msg.as_deref(),
823 Some("Invalid topic: invalid.topic.BTCUSDT")
824 );
825 }
826 other => panic!("Expected ErrorResponse, was {other:?}"),
827 }
828 }
829
830 #[rstest]
831 fn test_parse_frame_order_response() {
832 let value = load_value("ws_order_response.json");
833 let frame = parse_bybit_ws_frame(value);
834 match frame {
835 BybitWsFrame::OrderResponse(resp) => {
836 assert_eq!(resp.op.as_str(), "order.create");
837 assert_eq!(resp.ret_code, 0);
838 assert_eq!(resp.ret_msg, "OK");
839 }
840 other => panic!("Expected OrderResponse, was {other:?}"),
841 }
842 }
843
844 #[rstest]
845 fn test_parse_frame_orderbook() {
846 let value = load_value("ws_orderbook_snapshot.json");
847 let frame = parse_bybit_ws_frame(value);
848 assert!(
849 matches!(frame, BybitWsFrame::Orderbook(_)),
850 "Expected Orderbook, was {frame:?}"
851 );
852 }
853
854 #[rstest]
855 fn test_parse_frame_trade() {
856 let value = load_value("ws_public_trade.json");
857 let frame = parse_bybit_ws_frame(value);
858 assert!(
859 matches!(frame, BybitWsFrame::Trade(_)),
860 "Expected Trade, was {frame:?}"
861 );
862 }
863
864 #[rstest]
865 fn test_parse_frame_kline() {
866 let value = load_value("ws_kline.json");
867 let frame = parse_bybit_ws_frame(value);
868 assert!(
869 matches!(frame, BybitWsFrame::Kline(_)),
870 "Expected Kline, was {frame:?}"
871 );
872 }
873
874 #[rstest]
875 fn test_parse_frame_ticker_linear() {
876 let value = load_value("ws_ticker_linear.json");
877 let frame = parse_bybit_ws_frame(value);
878 assert!(
879 matches!(frame, BybitWsFrame::TickerLinear(_)),
880 "Expected TickerLinear, was {frame:?}"
881 );
882 }
883
884 #[rstest]
885 fn test_parse_frame_ticker_option() {
886 let value = load_value("ws_ticker_option.json");
887 let frame = parse_bybit_ws_frame(value);
888 assert!(
889 matches!(frame, BybitWsFrame::TickerOption(_)),
890 "Expected TickerOption, was {frame:?}"
891 );
892 }
893
894 #[rstest]
895 fn test_parse_frame_account_order() {
896 let value = load_value("ws_account_order.json");
897 let frame = parse_bybit_ws_frame(value);
898 assert!(
899 matches!(frame, BybitWsFrame::AccountOrder(_)),
900 "Expected AccountOrder, was {frame:?}"
901 );
902 }
903
904 #[rstest]
905 fn test_parse_frame_account_execution() {
906 let value = load_value("ws_account_execution.json");
907 let frame = parse_bybit_ws_frame(value);
908 assert!(
909 matches!(frame, BybitWsFrame::AccountExecution(_)),
910 "Expected AccountExecution, was {frame:?}"
911 );
912 }
913
914 #[rstest]
915 fn test_parse_frame_account_wallet() {
916 let value = load_value("ws_account_wallet.json");
917 let frame = parse_bybit_ws_frame(value);
918 assert!(
919 matches!(frame, BybitWsFrame::AccountWallet(_)),
920 "Expected AccountWallet, was {frame:?}"
921 );
922 }
923
924 #[rstest]
925 fn test_parse_frame_account_position() {
926 let value = load_value("ws_account_position.json");
927 let frame = parse_bybit_ws_frame(value);
928 assert!(
929 matches!(frame, BybitWsFrame::AccountPosition(_)),
930 "Expected AccountPosition, was {frame:?}"
931 );
932 }
933
934 #[rstest]
935 fn test_parse_frame_unknown_message() {
936 let value: serde_json::Value = serde_json::json!({"foo": "bar"});
937 let frame = parse_bybit_ws_frame(value);
938 assert!(
939 matches!(frame, BybitWsFrame::Unknown(_)),
940 "Expected Unknown, was {frame:?}"
941 );
942 }
943
944 #[rstest]
945 fn test_parse_raw_reconnected_signal() {
946 let msg = Message::Text(nautilus_network::RECONNECTED.to_string().into());
947 let result = BybitWsFeedHandler::parse_raw_frame(msg);
948 assert!(
949 matches!(result, Some(BybitWsFrame::Reconnected)),
950 "Expected Some(Reconnected), was {result:?}"
951 );
952 }
953
954 #[rstest]
955 fn test_parse_raw_pong_text() {
956 let msg = Message::Text("pong".into());
957 let result = BybitWsFeedHandler::parse_raw_frame(msg);
958 assert!(result.is_none(), "Expected None for pong, was {result:?}");
959 }
960
961 #[rstest]
962 fn test_parse_raw_json_pong_message() {
963 let msg = Message::Text(
964 r#"{"args":["1777226678908"],"conn_id":"yzr7jz02gws1vh60mk5m-hxqdp","op":"pong"}"#
965 .into(),
966 );
967 let result = BybitWsFeedHandler::parse_raw_frame(msg);
968 assert!(
969 result.is_none(),
970 "Expected None for JSON pong, was {result:?}"
971 );
972 }
973
974 #[rstest]
975 fn test_parse_raw_valid_json() {
976 let json = load_test_json("ws_public_trade.json");
977 let msg = Message::Text(json.into());
978 let result = BybitWsFeedHandler::parse_raw_frame(msg);
979 assert!(
980 matches!(result, Some(BybitWsFrame::Trade(_))),
981 "Expected Some(Trade), was {result:?}"
982 );
983 }
984
985 #[rstest]
986 fn test_parse_raw_invalid_json() {
987 let msg = Message::Text("not valid json".into());
988 let result = BybitWsFeedHandler::parse_raw_frame(msg);
989 assert!(
990 result.is_none(),
991 "Expected None for invalid JSON, was {result:?}"
992 );
993 }
994
995 #[rstest]
996 fn test_parse_raw_binary_message() {
997 let msg = Message::Binary(vec![0x01, 0x02].into());
998 let result = BybitWsFeedHandler::parse_raw_frame(msg);
999 assert!(result.is_none(), "Expected None for binary, was {result:?}");
1000 }
1001
1002 #[rstest]
1003 fn test_subscription_ack_with_req_id_confirms_only_that_topic() {
1004 let handler = create_test_handler();
1005 handler.subscriptions.mark_subscribe("orderbook.50.BTCUSDT");
1006 handler.subscriptions.mark_subscribe("publicTrade.BTCUSDT");
1007
1008 let ack = BybitWsSubscriptionMsg {
1009 success: true,
1010 op: BybitWsOperation::Subscribe,
1011 conn_id: None,
1012 req_id: Some("orderbook.50.BTCUSDT".to_string()),
1013 ret_msg: None,
1014 };
1015
1016 handler.handle_subscription_ack(&ack);
1017
1018 assert!(
1020 handler
1021 .subscriptions
1022 .pending_subscribe_topics()
1023 .contains(&"publicTrade.BTCUSDT".to_string())
1024 );
1025 assert!(
1026 !handler
1027 .subscriptions
1028 .pending_subscribe_topics()
1029 .contains(&"orderbook.50.BTCUSDT".to_string())
1030 );
1031 }
1032
1033 #[rstest]
1034 fn test_subscription_failure_with_req_id_marks_only_that_topic() {
1035 let handler = create_test_handler();
1036 handler.subscriptions.mark_subscribe("orderbook.50.BTCUSDT");
1037 handler.subscriptions.mark_subscribe("publicTrade.BTCUSDT");
1038
1039 let ack = BybitWsSubscriptionMsg {
1040 success: false,
1041 op: BybitWsOperation::Subscribe,
1042 conn_id: None,
1043 req_id: Some("orderbook.50.BTCUSDT".to_string()),
1044 ret_msg: Some("Invalid topic".to_string()),
1045 };
1046
1047 handler.handle_subscription_ack(&ack);
1048
1049 let pending = handler.subscriptions.pending_subscribe_topics();
1052 assert!(pending.contains(&"orderbook.50.BTCUSDT".to_string()));
1053 assert!(pending.contains(&"publicTrade.BTCUSDT".to_string()));
1054 }
1055
1056 #[rstest]
1057 fn test_error_response_with_subscribe_op_triggers_mark_failure() {
1058 let handler = create_test_handler();
1059 handler
1060 .subscriptions
1061 .mark_subscribe("invalid.topic.BTCUSDT");
1062
1063 let resp = BybitWsResponse {
1064 op: Some(BybitWsOperation::Subscribe),
1065 topic: None,
1066 success: Some(false),
1067 conn_id: None,
1068 req_id: Some("invalid.topic.BTCUSDT".to_string()),
1069 ret_code: Some(10001),
1070 ret_msg: Some("Invalid topic".to_string()),
1071 };
1072
1073 handler.handle_subscription_error(&resp);
1074
1075 let pending = handler.subscriptions.pending_subscribe_topics();
1077 assert!(pending.contains(&"invalid.topic.BTCUSDT".to_string()));
1078 }
1079
1080 #[rstest]
1081 fn test_already_subscribed_error_confirms_topic() {
1082 let handler = create_test_handler();
1083 handler.subscriptions.mark_subscribe("tickers.ETHUSDT");
1084
1085 let resp = BybitWsResponse {
1086 op: Some(BybitWsOperation::Subscribe),
1087 topic: None,
1088 success: Some(false),
1089 conn_id: None,
1090 req_id: Some("tickers.ETHUSDT".to_string()),
1091 ret_code: Some(10001),
1092 ret_msg: Some("error:already subscribed,topic:tickers.ETHUSDT".to_string()),
1093 };
1094
1095 handler.handle_subscription_error(&resp);
1096
1097 let pending = handler.subscriptions.pending_subscribe_topics();
1098 assert!(!pending.contains(&"tickers.ETHUSDT".to_string()));
1099 let symbols = handler.subscriptions.confirmed();
1100 let entry = symbols
1101 .get(&Ustr::from("tickers"))
1102 .expect("channel present");
1103 assert!(entry.contains(&Ustr::from("ETHUSDT")));
1104 }
1105
1106 #[rstest]
1107 fn test_subscription_ack_without_req_id_confirms_all_pending() {
1108 let handler = create_test_handler();
1109 handler.subscriptions.mark_subscribe("orderbook.50.BTCUSDT");
1110 handler.subscriptions.mark_subscribe("publicTrade.BTCUSDT");
1111
1112 let ack = BybitWsSubscriptionMsg {
1113 success: true,
1114 op: BybitWsOperation::Subscribe,
1115 conn_id: None,
1116 req_id: None,
1117 ret_msg: None,
1118 };
1119
1120 handler.handle_subscription_ack(&ack);
1121
1122 assert!(handler.subscriptions.pending_subscribe_topics().is_empty());
1124 }
1125}