Skip to main content

nautilus_coinapi/book/
mod.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Order book synchronization and recovery for CoinAPI `book` and `book_l3` streams.
17//!
18//! - [`sync`] owns sequence validation, snapshot acceptance, and recovery state transitions.
19//! - `orders` keeps the resting orders an L3 stream changes by identifier.
20//! - `recovery` restarts a book's stream through the shared retry runner.
21//!
22//! CoinAPI numbers each (type, symbol) stream from one per connection and sends a snapshot when
23//! the pair enters the session scope. It replays nothing for a repeated scope, so recovery takes
24//! the pair out of the scope and restores it, and the venue stream delivers the snapshot.
25//! Fixed-depth `book5`, `book20`, and `book50` messages each list complete top levels, so they need
26//! no synchronization.
27
28pub mod sync;
29
30pub(crate) mod orders;
31pub(crate) mod recovery;
32
33use crate::websocket::error::Error as WsError;
34
35/// Error from a CoinAPI book recovery attempt, classified for the shared retry runner.
36#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
37pub(crate) enum CoinApiBookError {
38    /// A later attempt can resolve the failure.
39    #[error("{0}")]
40    Retryable(String),
41    /// Another attempt cannot resolve the failure.
42    #[error("{0}")]
43    Permanent(String),
44}
45
46impl CoinApiBookError {
47    pub(crate) fn is_retryable(&self) -> bool {
48        matches!(self, Self::Retryable(_))
49    }
50}
51
52impl From<WsError> for CoinApiBookError {
53    fn from(error: WsError) -> Self {
54        match error {
55            WsError::Validation(_) => Self::Permanent(error.to_string()),
56            WsError::Transport(_) | WsError::Send(_) => Self::Retryable(error.to_string()),
57        }
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use nautilus_network::{TransportError, error::SendError};
64    use rstest::rstest;
65
66    use super::*;
67
68    #[rstest]
69    #[case(
70        WsError::Validation("book pair is not subscribed".into()),
71        CoinApiBookError::Permanent(
72            "Invalid CoinAPI WebSocket request: book pair is not subscribed".into()
73        ),
74        false
75    )]
76    #[case(
77        WsError::Transport(TransportError::ConnectionReset),
78        CoinApiBookError::Retryable("connection reset".into()),
79        true
80    )]
81    #[case(
82        WsError::Send(SendError::BrokenPipe("writer closed".into())),
83        CoinApiBookError::Retryable("send failed: broken pipe (writer closed)".into()),
84        true
85    )]
86    fn websocket_errors_preserve_context_and_retry_classification(
87        #[case] error: WsError,
88        #[case] expected: CoinApiBookError,
89        #[case] retryable: bool,
90    ) {
91        let mapped = CoinApiBookError::from(error);
92
93        assert_eq!(mapped, expected);
94        assert_eq!(mapped.is_retryable(), retryable);
95    }
96}