nautilus_tardis/machine/
message.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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
16use chrono::{DateTime, Utc};
17use serde::{Deserialize, Serialize};
18use ustr::Ustr;
19
20use crate::{enums::Exchange, parse::deserialize_uppercase};
21
22/// Represents a single level in the order book (bid or ask).
23#[derive(Debug, Clone, Deserialize, Serialize)]
24pub struct BookLevel {
25    /// The price at this level.
26    pub price: f64,
27    /// The amount at this level.
28    pub amount: f64,
29}
30
31/// Represents a Tardis WebSocket message for book changes.
32#[derive(Debug, Clone, Deserialize, Serialize)]
33#[serde(rename_all = "camelCase")]
34pub struct BookChangeMsg {
35    /// The symbol as provided by the exchange.
36    #[serde(deserialize_with = "deserialize_uppercase")]
37    pub symbol: Ustr,
38    /// The exchange ID.
39    pub exchange: Exchange,
40    /// Indicates whether this is an initial order book snapshot.
41    pub is_snapshot: bool,
42    /// Updated bids, with price and amount levels.
43    pub bids: Vec<BookLevel>,
44    /// Updated asks, with price and amount levels.
45    pub asks: Vec<BookLevel>,
46    /// The order book update timestamp provided by the exchange (ISO 8601 format).
47    pub timestamp: DateTime<Utc>,
48    /// The local timestamp when the message was received.
49    pub local_timestamp: DateTime<Utc>,
50}
51
52/// Represents a Tardis WebSocket message for book snapshots.
53#[derive(Debug, Clone, Deserialize, Serialize)]
54#[serde(rename_all = "camelCase")]
55pub struct BookSnapshotMsg {
56    /// The symbol as provided by the exchange.
57    #[serde(deserialize_with = "deserialize_uppercase")]
58    pub symbol: Ustr,
59    /// The exchange ID.
60    pub exchange: Exchange,
61    /// The name of the snapshot, e.g., `book_snapshot_{depth}_{interval}{time_unit}`.
62    pub name: String,
63    /// The requested number of levels (top bids/asks).
64    pub depth: u32,
65    /// The requested snapshot interval in milliseconds.
66    pub interval: u32,
67    /// The top bids price-amount levels.
68    pub bids: Vec<BookLevel>,
69    /// The top asks price-amount levels.
70    pub asks: Vec<BookLevel>,
71    /// The snapshot timestamp based on the last book change message processed timestamp.
72    pub timestamp: DateTime<Utc>,
73    /// The local timestamp when the message was received.
74    pub local_timestamp: DateTime<Utc>,
75}
76
77/// Represents a Tardis WebSocket message for trades.
78#[derive(Debug, Clone, Deserialize, Serialize)]
79#[serde(tag = "type")]
80#[serde(rename_all = "camelCase")]
81pub struct TradeMsg {
82    /// The symbol as provided by the exchange.
83    #[serde(deserialize_with = "deserialize_uppercase")]
84    pub symbol: Ustr,
85    /// The exchange ID.
86    pub exchange: Exchange,
87    /// The trade ID provided by the exchange (optional).
88    pub id: Option<String>,
89    /// The trade price as provided by the exchange.
90    pub price: f64,
91    /// The trade amount as provided by the exchange.
92    pub amount: f64,
93    /// The liquidity taker side (aggressor) for the trade.
94    pub side: String,
95    /// The trade timestamp provided by the exchange.
96    pub timestamp: DateTime<Utc>,
97    /// The local timestamp when the message was received.
98    pub local_timestamp: DateTime<Utc>,
99}
100
101/// Derivative instrument ticker info sourced from real-time ticker & instrument channels.
102#[derive(Debug, Clone, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct DerivativeTickerMsg {
105    /// The symbol as provided by the exchange.
106    #[serde(deserialize_with = "deserialize_uppercase")]
107    pub symbol: Ustr,
108    /// The exchange ID.
109    pub exchange: Exchange,
110    /// The last instrument price if provided by exchange.
111    pub last_price: Option<f64>,
112    /// The last open interest if provided by exchange.
113    pub open_interest: Option<f64>,
114    /// The last funding rate if provided by exchange.
115    pub funding_rate: Option<f64>,
116    /// The last index price if provided by exchange.
117    pub index_price: Option<f64>,
118    /// The last mark price if provided by exchange.
119    pub mark_price: Option<f64>,
120    /// The message timestamp provided by exchange.
121    pub timestamp: DateTime<Utc>,
122    /// The local timestamp when the message was received.
123    pub local_timestamp: DateTime<Utc>,
124}
125
126/// Trades data in aggregated form, known as OHLC, candlesticks, klines etc. Not only most common
127/// time based aggregation is supported, but volume and tick count based as well. Bars are computed
128/// from tick-by-tick raw trade data, if in given interval no trades happened, there is no bar produced.
129#[derive(Debug, Clone, Serialize, Deserialize)]
130#[serde(rename_all = "camelCase")]
131pub struct BarMsg {
132    /// The symbol as provided by the exchange.
133    #[serde(deserialize_with = "deserialize_uppercase")]
134    pub symbol: Ustr,
135    /// The exchange ID.
136    pub exchange: Exchange,
137    /// name with format `trade_bar`_{interval}
138    pub name: String,
139    /// The requested trade bar interval.
140    pub interval: u64,
141    /// The open price.
142    pub open: f64,
143    /// The high price.
144    pub high: f64,
145    /// The low price.
146    pub low: f64,
147    /// The close price.
148    pub close: f64,
149    /// The total volume traded in given interval.
150    pub volume: f64,
151    /// The buy volume traded in given interval.
152    pub buy_volume: f64,
153    /// The sell volume traded in given interval.
154    pub sell_volume: f64,
155    /// The trades count in given interval.
156    pub trades: u64,
157    /// The volume weighted average price.
158    pub vwap: f64,
159    /// The timestamp of first trade for given bar.
160    pub open_timestamp: DateTime<Utc>,
161    /// The timestamp of last trade for given bar.
162    pub close_timestamp: DateTime<Utc>,
163    /// The end of interval period timestamp.
164    pub timestamp: DateTime<Utc>,
165    /// The message arrival timestamp that triggered given bar computation.
166    pub local_timestamp: DateTime<Utc>,
167}
168
169/// Message that marks events when real-time WebSocket connection that was used to collect the
170/// historical data got disconnected.
171#[derive(Debug, Clone, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub struct DisconnectMsg {
174    /// The exchange ID.
175    pub exchange: Exchange,
176    /// The message arrival timestamp that triggered given bar computation (ISO 8601 format).
177    pub local_timestamp: DateTime<Utc>,
178}
179
180/// A Tardis Machine Server message type.
181#[allow(missing_docs)]
182#[derive(Debug, Clone, Serialize, Deserialize)]
183#[serde(rename_all = "snake_case", tag = "type")]
184pub enum WsMessage {
185    BookChange(BookChangeMsg),
186    BookSnapshot(BookSnapshotMsg),
187    Trade(TradeMsg),
188    TradeBar(BarMsg),
189    DerivativeTicker(DerivativeTickerMsg),
190    Disconnect(DisconnectMsg),
191}
192
193////////////////////////////////////////////////////////////////////////////////
194// Tests
195////////////////////////////////////////////////////////////////////////////////
196#[cfg(test)]
197mod tests {
198    use rstest::rstest;
199
200    use super::*;
201    use crate::tests::load_test_json;
202
203    #[rstest]
204    fn test_parse_book_change_message() {
205        let json_data = load_test_json("book_change.json");
206        let message: BookChangeMsg = serde_json::from_str(&json_data).unwrap();
207
208        assert_eq!(message.symbol, "XBTUSD");
209        assert_eq!(message.exchange, Exchange::Bitmex);
210        assert!(!message.is_snapshot);
211        assert!(message.bids.is_empty());
212        assert_eq!(message.asks.len(), 1);
213        assert_eq!(message.asks[0].price, 7_985.0);
214        assert_eq!(message.asks[0].amount, 283_318.0);
215        assert_eq!(
216            message.timestamp,
217            DateTime::parse_from_rfc3339("2019-10-23T11:29:53.469Z").unwrap()
218        );
219        assert_eq!(
220            message.local_timestamp,
221            DateTime::parse_from_rfc3339("2019-10-23T11:29:53.469Z").unwrap()
222        );
223    }
224
225    #[rstest]
226    fn test_parse_book_snapshot_message() {
227        let json_data = load_test_json("book_snapshot.json");
228        let message: BookSnapshotMsg = serde_json::from_str(&json_data).unwrap();
229
230        assert_eq!(message.symbol, "XBTUSD");
231        assert_eq!(message.exchange, Exchange::Bitmex);
232        assert_eq!(message.name, "book_snapshot_2_50ms");
233        assert_eq!(message.depth, 2);
234        assert_eq!(message.interval, 50);
235        assert_eq!(message.bids.len(), 2);
236        assert_eq!(message.asks.len(), 2);
237        assert_eq!(message.bids[0].price, 7_633.5);
238        assert_eq!(message.bids[0].amount, 1_906_067.0);
239        assert_eq!(message.asks[0].price, 7_634.0);
240        assert_eq!(message.asks[0].amount, 1_467_849.0);
241        assert_eq!(
242            message.timestamp,
243            DateTime::parse_from_rfc3339("2019-10-25T13:39:46.950Z").unwrap(),
244        );
245        assert_eq!(
246            message.local_timestamp,
247            DateTime::parse_from_rfc3339("2019-10-25T13:39:46.961Z").unwrap()
248        );
249    }
250
251    #[rstest]
252    fn test_parse_trade_message() {
253        let json_data = load_test_json("trade.json");
254        let message: TradeMsg = serde_json::from_str(&json_data).unwrap();
255
256        assert_eq!(message.symbol, "XBTUSD");
257        assert_eq!(message.exchange, Exchange::Bitmex);
258        assert_eq!(
259            message.id,
260            Some("282a0445-0e3a-abeb-f403-11003204ea1b".to_string())
261        );
262        assert_eq!(message.price, 7_996.0);
263        assert_eq!(message.amount, 50.0);
264        assert_eq!(message.side, "sell");
265        assert_eq!(
266            message.timestamp,
267            DateTime::parse_from_rfc3339("2019-10-23T10:32:49.669Z").unwrap()
268        );
269        assert_eq!(
270            message.local_timestamp,
271            DateTime::parse_from_rfc3339("2019-10-23T10:32:49.740Z").unwrap()
272        );
273    }
274
275    #[rstest]
276    fn test_parse_derivative_ticker_message() {
277        let json_data = load_test_json("derivative_ticker.json");
278        let message: DerivativeTickerMsg = serde_json::from_str(&json_data).unwrap();
279
280        assert_eq!(message.symbol, "BTC-PERPETUAL");
281        assert_eq!(message.exchange, Exchange::Deribit);
282        assert_eq!(message.last_price, Some(7_987.5));
283        assert_eq!(message.open_interest, Some(84_129_491.0));
284        assert_eq!(message.funding_rate, Some(-0.00001568));
285        assert_eq!(message.index_price, Some(7_989.28));
286        assert_eq!(message.mark_price, Some(7_987.56));
287        assert_eq!(
288            message.timestamp,
289            DateTime::parse_from_rfc3339("2019-10-23T11:34:29.302Z").unwrap()
290        );
291        assert_eq!(
292            message.local_timestamp,
293            DateTime::parse_from_rfc3339("2019-10-23T11:34:29.416Z").unwrap()
294        );
295    }
296
297    #[rstest]
298    fn test_parse_bar_message() {
299        let json_data = load_test_json("bar.json");
300        let message: BarMsg = serde_json::from_str(&json_data).unwrap();
301
302        assert_eq!(message.symbol, "XBTUSD");
303        assert_eq!(message.exchange, Exchange::Bitmex);
304        assert_eq!(message.name, "trade_bar_10000ms");
305        assert_eq!(message.interval, 10_000);
306        assert_eq!(message.open, 7_623.5);
307        assert_eq!(message.high, 7_623.5);
308        assert_eq!(message.low, 7_623.0);
309        assert_eq!(message.close, 7_623.5);
310        assert_eq!(message.volume, 37_034.0);
311        assert_eq!(message.buy_volume, 24_244.0);
312        assert_eq!(message.sell_volume, 12_790.0);
313        assert_eq!(message.trades, 9);
314        assert_eq!(message.vwap, 7_623.327320840309);
315        assert_eq!(
316            message.open_timestamp,
317            DateTime::parse_from_rfc3339("2019-10-25T13:11:31.574Z").unwrap()
318        );
319        assert_eq!(
320            message.close_timestamp,
321            DateTime::parse_from_rfc3339("2019-10-25T13:11:39.212Z").unwrap()
322        );
323        assert_eq!(
324            message.local_timestamp,
325            DateTime::parse_from_rfc3339("2019-10-25T13:11:40.369Z").unwrap()
326        );
327        assert_eq!(
328            message.timestamp,
329            DateTime::parse_from_rfc3339("2019-10-25T13:11:40.000Z").unwrap()
330        );
331    }
332
333    #[rstest]
334    fn test_parse_disconnect_message() {
335        let json_data = load_test_json("disconnect.json");
336        let message: DisconnectMsg = serde_json::from_str(&json_data).unwrap();
337
338        assert_eq!(message.exchange, Exchange::Deribit);
339        assert_eq!(
340            message.local_timestamp,
341            DateTime::parse_from_rfc3339("2019-10-23T11:34:29.416Z").unwrap()
342        );
343    }
344}