1use crate::errors::{PolyfillError, Result};
7use crate::types::*;
8use crate::ws_hot_path::{WsBookApplyStats, WsBookUpdateProcessor};
9use chrono::Utc;
10use futures::{SinkExt, Stream, StreamExt};
11use parking_lot::Mutex;
12use serde_json::Value;
13use std::collections::VecDeque;
14use std::pin::Pin;
15use std::task::{Context, Poll};
16use tokio::sync::mpsc;
17use tracing::{debug, error, info, warn};
18
19pub trait MarketStream: Stream<Item = Result<StreamMessage>> + Send + Sync {
21 fn subscribe(&mut self, subscription: Subscription) -> Result<()>;
23
24 fn unsubscribe(&mut self, token_ids: &[String]) -> Result<()>;
26
27 fn is_connected(&self) -> bool;
29
30 fn get_stats(&self) -> StreamStats;
32}
33
34#[derive(Debug)]
36#[allow(dead_code)]
37pub struct WebSocketStream {
38 connection: Option<
40 tokio_tungstenite::WebSocketStream<
41 tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
42 >,
43 >,
44 url: String,
46 auth: Option<WssAuth>,
48 subscriptions: Vec<WssSubscription>,
50 pending: VecDeque<StreamMessage>,
55 pending_capacity: usize,
56 stats: StreamStats,
58 reconnect_config: ReconnectConfig,
60}
61
62#[derive(Debug, Clone)]
64pub struct StreamStats {
65 pub messages_received: u64,
66 pub messages_sent: u64,
67 pub errors: u64,
68 pub dropped_messages: u64,
69 pub last_message_time: Option<chrono::DateTime<Utc>>,
70 pub connection_uptime: std::time::Duration,
71 pub reconnect_count: u32,
72}
73
74#[derive(Debug, Clone)]
76pub struct ReconnectConfig {
77 pub max_retries: u32,
78 pub base_delay: std::time::Duration,
79 pub max_delay: std::time::Duration,
80 pub backoff_multiplier: f64,
81}
82
83impl Default for ReconnectConfig {
84 fn default() -> Self {
85 Self {
86 max_retries: 5,
87 base_delay: std::time::Duration::from_secs(1),
88 max_delay: std::time::Duration::from_secs(60),
89 backoff_multiplier: 2.0,
90 }
91 }
92}
93
94impl WebSocketStream {
95 pub fn new(url: &str) -> Self {
97 let pending_capacity = 1024;
98
99 Self {
100 connection: None,
101 url: url.to_string(),
102 auth: None,
103 subscriptions: Vec::new(),
104 pending: VecDeque::with_capacity(pending_capacity),
105 pending_capacity,
106 stats: StreamStats {
107 messages_received: 0,
108 messages_sent: 0,
109 errors: 0,
110 dropped_messages: 0,
111 last_message_time: None,
112 connection_uptime: std::time::Duration::ZERO,
113 reconnect_count: 0,
114 },
115 reconnect_config: ReconnectConfig::default(),
116 }
117 }
118
119 fn enqueue(&mut self, message: StreamMessage) {
120 if self.pending.len() >= self.pending_capacity {
121 let _ = self.pending.pop_front();
122 self.stats.dropped_messages += 1;
123 }
124 self.pending.push_back(message);
125 }
126
127 pub fn with_auth(mut self, auth: WssAuth) -> Self {
129 self.auth = Some(auth);
130 self
131 }
132
133 async fn connect(&mut self) -> Result<()> {
135 let (ws_stream, _) = tokio_tungstenite::connect_async(&self.url)
136 .await
137 .map_err(|e| {
138 PolyfillError::stream(
139 format!("WebSocket connection failed: {}", e),
140 crate::errors::StreamErrorKind::ConnectionFailed,
141 )
142 })?;
143
144 self.connection = Some(ws_stream);
145 info!("Connected to WebSocket stream at {}", self.url);
146 Ok(())
147 }
148
149 async fn send_message(&mut self, message: Value) -> Result<()> {
151 if let Some(connection) = &mut self.connection {
152 let text = serde_json::to_string(&message).map_err(|e| {
153 PolyfillError::parse(format!("Failed to serialize message: {}", e), None)
154 })?;
155
156 let ws_message = tokio_tungstenite::tungstenite::Message::Text(text);
157 connection.send(ws_message).await.map_err(|e| {
158 PolyfillError::stream(
159 format!("Failed to send message: {}", e),
160 crate::errors::StreamErrorKind::MessageCorrupted,
161 )
162 })?;
163
164 self.stats.messages_sent += 1;
165 }
166
167 Ok(())
168 }
169
170 pub async fn subscribe_async(&mut self, subscription: WssSubscription) -> Result<()> {
172 if self.connection.is_none() {
174 self.connect().await?;
175 }
176
177 let message = serde_json::to_value(&subscription).map_err(|e| {
180 PolyfillError::parse(format!("Failed to serialize subscription: {}", e), None)
181 })?;
182
183 self.send_message(message).await?;
184 self.subscriptions.push(subscription.clone());
185
186 info!("Subscribed to {} channel", subscription.channel_type);
187 Ok(())
188 }
189
190 pub async fn subscribe_user_channel(&mut self, markets: Vec<String>) -> Result<()> {
192 let auth = self
193 .auth
194 .as_ref()
195 .ok_or_else(|| PolyfillError::auth("No authentication provided for WebSocket"))?
196 .clone();
197
198 let subscription = WssSubscription {
199 channel_type: "user".to_string(),
200 operation: Some("subscribe".to_string()),
201 markets,
202 asset_ids: Vec::new(),
203 initial_dump: Some(true),
204 custom_feature_enabled: None,
205 auth: Some(auth),
206 };
207
208 self.subscribe_async(subscription).await
209 }
210
211 pub async fn subscribe_market_channel(&mut self, asset_ids: Vec<String>) -> Result<()> {
214 let subscription = WssSubscription {
215 channel_type: "market".to_string(),
216 operation: Some("subscribe".to_string()),
217 markets: Vec::new(),
218 asset_ids,
219 initial_dump: Some(true),
220 custom_feature_enabled: None,
221 auth: None,
222 };
223
224 self.subscribe_async(subscription).await
225 }
226
227 pub async fn subscribe_market_channel_with_features(
230 &mut self,
231 asset_ids: Vec<String>,
232 ) -> Result<()> {
233 let subscription = WssSubscription {
234 channel_type: "market".to_string(),
235 operation: Some("subscribe".to_string()),
236 markets: Vec::new(),
237 asset_ids,
238 initial_dump: Some(true),
239 custom_feature_enabled: Some(true),
240 auth: None,
241 };
242
243 self.subscribe_async(subscription).await
244 }
245
246 pub async fn unsubscribe_market_channel(&mut self, asset_ids: Vec<String>) -> Result<()> {
248 let subscription = WssSubscription {
249 channel_type: "market".to_string(),
250 operation: Some("unsubscribe".to_string()),
251 markets: Vec::new(),
252 asset_ids,
253 initial_dump: None,
254 custom_feature_enabled: None,
255 auth: None,
256 };
257
258 self.subscribe_async(subscription).await
259 }
260
261 pub async fn unsubscribe_user_channel(&mut self, markets: Vec<String>) -> Result<()> {
263 let auth = self
264 .auth
265 .as_ref()
266 .ok_or_else(|| PolyfillError::auth("No authentication provided for WebSocket"))?
267 .clone();
268
269 let subscription = WssSubscription {
270 channel_type: "user".to_string(),
271 operation: Some("unsubscribe".to_string()),
272 markets,
273 asset_ids: Vec::new(),
274 initial_dump: None,
275 custom_feature_enabled: None,
276 auth: Some(auth),
277 };
278
279 self.subscribe_async(subscription).await
280 }
281
282 #[allow(dead_code)]
284 async fn handle_message(
285 &mut self,
286 message: tokio_tungstenite::tungstenite::Message,
287 ) -> Result<()> {
288 match message {
289 tokio_tungstenite::tungstenite::Message::Text(text) => {
290 debug!("Received WebSocket message: {}", text);
291
292 let stream_messages = crate::decode::parse_stream_messages(&text)?;
294 for stream_message in stream_messages {
295 self.enqueue(stream_message);
296 }
297
298 self.stats.messages_received += 1;
299 self.stats.last_message_time = Some(Utc::now());
300 },
301 tokio_tungstenite::tungstenite::Message::Close(_) => {
302 info!("WebSocket connection closed by server");
303 self.connection = None;
304 },
305 tokio_tungstenite::tungstenite::Message::Ping(data) => {
306 if let Some(connection) = &mut self.connection {
308 let pong = tokio_tungstenite::tungstenite::Message::Pong(data);
309 if let Err(e) = connection.send(pong).await {
310 error!("Failed to send pong: {}", e);
311 }
312 }
313 },
314 tokio_tungstenite::tungstenite::Message::Pong(_) => {
315 debug!("Received pong");
317 },
318 tokio_tungstenite::tungstenite::Message::Binary(_) => {
319 warn!("Received binary message (not supported)");
320 },
321 tokio_tungstenite::tungstenite::Message::Frame(_) => {
322 warn!("Received raw frame (not supported)");
323 },
324 }
325
326 Ok(())
327 }
328
329 #[allow(dead_code)]
331 fn parse_polymarket_messages(&self, text: &str) -> Result<Vec<StreamMessage>> {
332 crate::decode::parse_stream_messages(text)
333 }
334
335 #[allow(dead_code)]
337 async fn reconnect(&mut self) -> Result<()> {
338 let mut delay = self.reconnect_config.base_delay;
339 let mut retries = 0;
340
341 while retries < self.reconnect_config.max_retries {
342 warn!("Attempting to reconnect (attempt {})", retries + 1);
343
344 match self.connect().await {
345 Ok(()) => {
346 info!("Successfully reconnected");
347 self.stats.reconnect_count += 1;
348
349 let subscriptions = self.subscriptions.clone();
351 for subscription in subscriptions {
352 self.send_message(serde_json::to_value(subscription)?)
353 .await?;
354 }
355
356 return Ok(());
357 },
358 Err(e) => {
359 error!("Reconnection attempt {} failed: {}", retries + 1, e);
360 retries += 1;
361
362 if retries < self.reconnect_config.max_retries {
363 tokio::time::sleep(delay).await;
364 delay = std::cmp::min(
365 delay.mul_f64(self.reconnect_config.backoff_multiplier),
366 self.reconnect_config.max_delay,
367 );
368 }
369 },
370 }
371 }
372
373 Err(PolyfillError::stream(
374 format!(
375 "Failed to reconnect after {} attempts",
376 self.reconnect_config.max_retries
377 ),
378 crate::errors::StreamErrorKind::ConnectionFailed,
379 ))
380 }
381}
382
383pub struct WebSocketBookApplier<'a> {
390 stream: WebSocketStream,
391 books: &'a crate::book::OrderBookManager,
392 processor: WsBookUpdateProcessor,
393}
394
395impl WebSocketStream {
396 pub fn into_book_applier<'a>(
401 mut self,
402 books: &'a crate::book::OrderBookManager,
403 processor: WsBookUpdateProcessor,
404 ) -> WebSocketBookApplier<'a> {
405 self.pending.clear();
407 WebSocketBookApplier {
408 stream: self,
409 books,
410 processor,
411 }
412 }
413}
414
415impl<'a> WebSocketBookApplier<'a> {
416 pub fn stream_mut(&mut self) -> &mut WebSocketStream {
418 &mut self.stream
419 }
420
421 pub fn stream_stats(&self) -> StreamStats {
423 self.stream.stats.clone()
424 }
425
426 pub fn processor_mut(&mut self) -> &mut WsBookUpdateProcessor {
428 &mut self.processor
429 }
430
431 pub fn apply_text_message(&mut self, text: String) -> Result<WsBookApplyStats> {
437 let stats = self.processor.process_text(text, self.books)?;
438 self.stream.stats.messages_received += 1;
439 self.stream.stats.last_message_time = Some(Utc::now());
440 Ok(stats)
441 }
442
443 pub fn apply_bytes_message(&mut self, bytes: &mut [u8]) -> Result<WsBookApplyStats> {
449 let stats = self.processor.process_bytes(bytes, self.books)?;
450 self.stream.stats.messages_received += 1;
451 self.stream.stats.last_message_time = Some(Utc::now());
452 Ok(stats)
453 }
454}
455
456impl<'a> Stream for WebSocketBookApplier<'a> {
457 type Item = Result<WsBookApplyStats>;
458
459 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
460 loop {
461 let Some(connection) = &mut self.stream.connection else {
462 return Poll::Ready(None);
463 };
464
465 match connection.poll_next_unpin(cx) {
466 Poll::Pending => return Poll::Pending,
467 Poll::Ready(Some(Ok(msg))) => match msg {
468 tokio_tungstenite::tungstenite::Message::Text(text) => {
469 match self.apply_text_message(text) {
470 Ok(stats) => {
471 if stats.book_messages == 0 {
472 continue;
473 }
474 return Poll::Ready(Some(Ok(stats)));
475 },
476 Err(e) => {
477 self.stream.stats.errors += 1;
478 return Poll::Ready(Some(Err(e)));
479 },
480 }
481 },
482 tokio_tungstenite::tungstenite::Message::Close(_) => {
483 info!("WebSocket connection closed by server");
484 self.stream.connection = None;
485 return Poll::Ready(None);
486 },
487 tokio_tungstenite::tungstenite::Message::Ping(_) => {
488 continue;
490 },
491 tokio_tungstenite::tungstenite::Message::Pong(_) => continue,
492 tokio_tungstenite::tungstenite::Message::Binary(_) => continue,
493 tokio_tungstenite::tungstenite::Message::Frame(_) => continue,
494 },
495 Poll::Ready(Some(Err(e))) => {
496 error!("WebSocket error: {}", e);
497 self.stream.stats.errors += 1;
498 return Poll::Ready(Some(Err(e.into())));
499 },
500 Poll::Ready(None) => {
501 info!("WebSocket stream ended");
502 return Poll::Ready(None);
503 },
504 }
505 }
506 }
507}
508
509impl Stream for WebSocketStream {
510 type Item = Result<StreamMessage>;
511
512 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
513 loop {
514 if let Some(message) = self.pending.pop_front() {
515 return Poll::Ready(Some(Ok(message)));
516 }
517
518 let Some(connection) = &mut self.connection else {
519 return Poll::Ready(None);
520 };
521
522 match connection.poll_next_unpin(cx) {
523 Poll::Pending => return Poll::Pending,
524 Poll::Ready(Some(Ok(ws_message))) => match ws_message {
525 tokio_tungstenite::tungstenite::Message::Text(text) => {
526 match crate::decode::parse_stream_messages(&text) {
527 Ok(messages) => {
528 let mut iter = messages.into_iter();
529 let Some(first) = iter.next() else {
530 continue;
531 };
532
533 for msg in iter {
534 self.enqueue(msg);
535 }
536 self.stats.messages_received += 1;
537 self.stats.last_message_time = Some(Utc::now());
538 return Poll::Ready(Some(Ok(first)));
539 },
540 Err(e) => {
541 self.stats.errors += 1;
542 return Poll::Ready(Some(Err(e)));
543 },
544 }
545 },
546 tokio_tungstenite::tungstenite::Message::Close(_) => {
547 info!("WebSocket connection closed by server");
548 self.connection = None;
549 return Poll::Ready(None);
550 },
551 tokio_tungstenite::tungstenite::Message::Ping(_) => {
552 continue;
554 },
555 tokio_tungstenite::tungstenite::Message::Pong(_) => continue,
556 tokio_tungstenite::tungstenite::Message::Binary(_) => continue,
557 tokio_tungstenite::tungstenite::Message::Frame(_) => continue,
558 },
559 Poll::Ready(Some(Err(e))) => {
560 error!("WebSocket error: {}", e);
561 self.stats.errors += 1;
562 return Poll::Ready(Some(Err(e.into())));
563 },
564 Poll::Ready(None) => {
565 info!("WebSocket stream ended");
566 return Poll::Ready(None);
567 },
568 }
569 }
570 }
571}
572
573impl MarketStream for WebSocketStream {
574 fn subscribe(&mut self, _subscription: Subscription) -> Result<()> {
575 Ok(())
577 }
578
579 fn unsubscribe(&mut self, _token_ids: &[String]) -> Result<()> {
580 Ok(())
582 }
583
584 fn is_connected(&self) -> bool {
585 self.connection.is_some()
586 }
587
588 fn get_stats(&self) -> StreamStats {
589 self.stats.clone()
590 }
591}
592
593#[derive(Debug)]
595pub struct MockStream {
596 messages: Vec<Result<StreamMessage>>,
597 index: usize,
598 connected: bool,
599}
600
601impl Default for MockStream {
602 fn default() -> Self {
603 Self::new()
604 }
605}
606
607impl MockStream {
608 pub fn new() -> Self {
609 Self {
610 messages: Vec::new(),
611 index: 0,
612 connected: true,
613 }
614 }
615
616 pub fn add_message(&mut self, message: StreamMessage) {
617 self.messages.push(Ok(message));
618 }
619
620 pub fn add_error(&mut self, error: PolyfillError) {
621 self.messages.push(Err(error));
622 }
623
624 pub fn set_connected(&mut self, connected: bool) {
625 self.connected = connected;
626 }
627}
628
629impl Stream for MockStream {
630 type Item = Result<StreamMessage>;
631
632 fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
633 if self.index >= self.messages.len() {
634 Poll::Ready(None)
635 } else {
636 let message = self.messages[self.index].clone();
637 self.index += 1;
638 Poll::Ready(Some(message))
639 }
640 }
641}
642
643impl MarketStream for MockStream {
644 fn subscribe(&mut self, _subscription: Subscription) -> Result<()> {
645 Ok(())
646 }
647
648 fn unsubscribe(&mut self, _token_ids: &[String]) -> Result<()> {
649 Ok(())
650 }
651
652 fn is_connected(&self) -> bool {
653 self.connected
654 }
655
656 fn get_stats(&self) -> StreamStats {
657 StreamStats {
658 messages_received: self.messages.len() as u64,
659 messages_sent: 0,
660 errors: self.messages.iter().filter(|m| m.is_err()).count() as u64,
661 dropped_messages: 0,
662 last_message_time: None,
663 connection_uptime: std::time::Duration::ZERO,
664 reconnect_count: 0,
665 }
666 }
667}
668
669#[allow(dead_code)]
671pub struct StreamManager {
672 streams: Vec<Box<dyn MarketStream>>,
673 message_subscribers: Mutex<Vec<mpsc::UnboundedSender<StreamMessage>>>,
674}
675
676impl Default for StreamManager {
677 fn default() -> Self {
678 Self::new()
679 }
680}
681
682impl StreamManager {
683 pub fn new() -> Self {
684 Self {
685 streams: Vec::new(),
686 message_subscribers: Mutex::new(Vec::new()),
687 }
688 }
689
690 pub fn add_stream(&mut self, stream: Box<dyn MarketStream>) {
691 self.streams.push(stream);
692 }
693
694 pub fn get_message_receiver(&mut self) -> mpsc::UnboundedReceiver<StreamMessage> {
695 let (tx, rx) = mpsc::unbounded_channel();
696 self.message_subscribers.lock().push(tx);
697 rx
698 }
699
700 pub fn broadcast_message(&self, message: StreamMessage) -> Result<()> {
701 let mut subscribers = self.message_subscribers.lock();
702 subscribers.retain(|tx| tx.send(message.clone()).is_ok());
703 Ok(())
704 }
705}
706
707#[cfg(test)]
708mod tests {
709 use super::*;
710 use rust_decimal::Decimal;
711 use std::str::FromStr;
712
713 #[test]
714 fn test_mock_stream() {
715 let mut stream = MockStream::new();
716
717 stream.add_message(StreamMessage::Book(BookUpdate {
719 asset_id: "1".to_string(),
720 market: "0xabc".to_string(),
721 timestamp: 1_234_567_890,
722 bids: vec![],
723 asks: vec![],
724 hash: None,
725 }));
726 stream.add_message(StreamMessage::PriceChange(PriceChange {
727 market: "0xabc".to_string(),
728 timestamp: 1_234_567_891,
729 price_changes: vec![],
730 }));
731
732 assert!(stream.is_connected());
733 assert_eq!(stream.get_stats().messages_received, 2);
734 }
735
736 #[test]
737 fn test_stream_manager() {
738 let mut manager = StreamManager::new();
739 let mock_stream = Box::new(MockStream::new());
740 manager.add_stream(mock_stream);
741 let mut first_rx = manager.get_message_receiver();
742 let mut second_rx = manager.get_message_receiver();
743
744 let message = StreamMessage::Book(BookUpdate {
746 asset_id: "1".to_string(),
747 market: "0xabc".to_string(),
748 timestamp: 1_234_567_890,
749 bids: vec![],
750 asks: vec![],
751 hash: None,
752 });
753 assert!(manager.broadcast_message(message).is_ok());
754
755 assert!(matches!(
756 first_rx.try_recv().unwrap(),
757 StreamMessage::Book(BookUpdate { asset_id, .. }) if asset_id == "1"
758 ));
759 assert!(matches!(
760 second_rx.try_recv().unwrap(),
761 StreamMessage::Book(BookUpdate { asset_id, .. }) if asset_id == "1"
762 ));
763
764 drop(first_rx);
765 let message = StreamMessage::PriceChange(PriceChange {
766 market: "0xabc".to_string(),
767 timestamp: 1_234_567_891,
768 price_changes: vec![],
769 });
770 assert!(manager.broadcast_message(message).is_ok());
771
772 assert!(matches!(
773 second_rx.try_recv().unwrap(),
774 StreamMessage::PriceChange(PriceChange { market, .. }) if market == "0xabc"
775 ));
776 }
777
778 #[test]
779 fn test_websocket_book_applier_apply_text_message_updates_book() {
780 let books = crate::book::OrderBookManager::new(64);
781 let _ = books.get_or_create_book("12345").unwrap();
782
783 let processor = WsBookUpdateProcessor::new(1024);
784 let stream = WebSocketStream::new("wss://example.com/ws");
785 let mut applier = stream.into_book_applier(&books, processor);
786
787 let msg = r#"{"event_type":"book","asset_id":"12345","timestamp":1,"bids":[{"price":"0.75","size":"10"}],"asks":[{"price":"0.76","size":"5"}]}"#.to_string();
788 let stats = applier.apply_text_message(msg).unwrap();
789 assert_eq!(stats.book_messages, 1);
790 assert_eq!(stats.book_levels_applied, 2);
791
792 let snapshot = books.get_book("12345").unwrap();
793 assert_eq!(snapshot.bids.len(), 1);
794 assert_eq!(snapshot.asks.len(), 1);
795 assert_eq!(snapshot.bids[0].price, Decimal::from_str("0.75").unwrap());
796 assert_eq!(snapshot.bids[0].size, Decimal::from_str("10").unwrap());
797 assert_eq!(snapshot.asks[0].price, Decimal::from_str("0.76").unwrap());
798 assert_eq!(snapshot.asks[0].size, Decimal::from_str("5").unwrap());
799 }
800
801 #[test]
802 fn test_websocket_book_applier_replaces_snapshot() {
803 let books = crate::book::OrderBookManager::new(64);
804 let _ = books.get_or_create_book("12345").unwrap();
805
806 let processor = WsBookUpdateProcessor::new(1024);
807 let stream = WebSocketStream::new("wss://example.com/ws");
808 let mut applier = stream.into_book_applier(&books, processor);
809
810 let first = r#"{"event_type":"book","asset_id":"12345","timestamp":"1757908892351","bids":[{"price":"0.74","size":"8"},{"price":"0.75","size":"10"}],"asks":[{"price":"0.76","size":"5"},{"price":"0.77","size":"4"}]}"#.to_string();
811 applier.apply_text_message(first).unwrap();
812
813 let second = r#"{"event_type":"book","asset_id":"12345","timestamp":"1757908892352","bids":[{"price":"0.75","size":"11"}],"asks":[{"price":"0.76","size":"6"}]}"#.to_string();
814 let stats = applier.apply_text_message(second).unwrap();
815 assert_eq!(stats.book_messages, 1);
816 assert_eq!(stats.book_levels_applied, 2);
817
818 let snapshot = books.get_book("12345").unwrap();
819 assert_eq!(snapshot.timestamp.timestamp_millis(), 1_757_908_892_352);
820 assert_eq!(snapshot.bids.len(), 1);
821 assert_eq!(snapshot.asks.len(), 1);
822 assert_eq!(snapshot.bids[0].price, Decimal::from_str("0.75").unwrap());
823 assert_eq!(snapshot.bids[0].size, Decimal::from_str("11").unwrap());
824 assert_eq!(snapshot.asks[0].price, Decimal::from_str("0.76").unwrap());
825 assert_eq!(snapshot.asks[0].size, Decimal::from_str("6").unwrap());
826 }
827}