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::TardisExchange, 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: TardisExchange,
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: TardisExchange,
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: TardisExchange,
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: TardisExchange,
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: TardisExchange,
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: TardisExchange,
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
197#[cfg(test)]
198mod tests {
199    use rstest::rstest;
200
201    use super::*;
202    use crate::tests::load_test_json;
203
204    #[rstest]
205    fn test_parse_book_change_message() {
206        let json_data = load_test_json("book_change.json");
207        let message: BookChangeMsg = serde_json::from_str(&json_data).unwrap();
208
209        assert_eq!(message.symbol, "XBTUSD");
210        assert_eq!(message.exchange, TardisExchange::Bitmex);
211        assert!(!message.is_snapshot);
212        assert!(message.bids.is_empty());
213        assert_eq!(message.asks.len(), 1);
214        assert_eq!(message.asks[0].price, 7_985.0);
215        assert_eq!(message.asks[0].amount, 283_318.0);
216        assert_eq!(
217            message.timestamp,
218            DateTime::parse_from_rfc3339("2019-10-23T11:29:53.469Z").unwrap()
219        );
220        assert_eq!(
221            message.local_timestamp,
222            DateTime::parse_from_rfc3339("2019-10-23T11:29:53.469Z").unwrap()
223        );
224    }
225
226    #[rstest]
227    fn test_parse_book_snapshot_message() {
228        let json_data = load_test_json("book_snapshot.json");
229        let message: BookSnapshotMsg = serde_json::from_str(&json_data).unwrap();
230
231        assert_eq!(message.symbol, "XBTUSD");
232        assert_eq!(message.exchange, TardisExchange::Bitmex);
233        assert_eq!(message.name, "book_snapshot_2_50ms");
234        assert_eq!(message.depth, 2);
235        assert_eq!(message.interval, 50);
236        assert_eq!(message.bids.len(), 2);
237        assert_eq!(message.asks.len(), 2);
238        assert_eq!(message.bids[0].price, 7_633.5);
239        assert_eq!(message.bids[0].amount, 1_906_067.0);
240        assert_eq!(message.asks[0].price, 7_634.0);
241        assert_eq!(message.asks[0].amount, 1_467_849.0);
242        assert_eq!(
243            message.timestamp,
244            DateTime::parse_from_rfc3339("2019-10-25T13:39:46.950Z").unwrap(),
245        );
246        assert_eq!(
247            message.local_timestamp,
248            DateTime::parse_from_rfc3339("2019-10-25T13:39:46.961Z").unwrap()
249        );
250    }
251
252    #[rstest]
253    fn test_parse_trade_message() {
254        let json_data = load_test_json("trade.json");
255        let message: TradeMsg = serde_json::from_str(&json_data).unwrap();
256
257        assert_eq!(message.symbol, "XBTUSD");
258        assert_eq!(message.exchange, TardisExchange::Bitmex);
259        assert_eq!(
260            message.id,
261            Some("282a0445-0e3a-abeb-f403-11003204ea1b".to_string())
262        );
263        assert_eq!(message.price, 7_996.0);
264        assert_eq!(message.amount, 50.0);
265        assert_eq!(message.side, "sell");
266        assert_eq!(
267            message.timestamp,
268            DateTime::parse_from_rfc3339("2019-10-23T10:32:49.669Z").unwrap()
269        );
270        assert_eq!(
271            message.local_timestamp,
272            DateTime::parse_from_rfc3339("2019-10-23T10:32:49.740Z").unwrap()
273        );
274    }
275
276    #[rstest]
277    fn test_parse_derivative_ticker_message() {
278        let json_data = load_test_json("derivative_ticker.json");
279        let message: DerivativeTickerMsg = serde_json::from_str(&json_data).unwrap();
280
281        assert_eq!(message.symbol, "BTC-PERPETUAL");
282        assert_eq!(message.exchange, TardisExchange::Deribit);
283        assert_eq!(message.last_price, Some(7_987.5));
284        assert_eq!(message.open_interest, Some(84_129_491.0));
285        assert_eq!(message.funding_rate, Some(-0.00001568));
286        assert_eq!(message.index_price, Some(7_989.28));
287        assert_eq!(message.mark_price, Some(7_987.56));
288        assert_eq!(
289            message.timestamp,
290            DateTime::parse_from_rfc3339("2019-10-23T11:34:29.302Z").unwrap()
291        );
292        assert_eq!(
293            message.local_timestamp,
294            DateTime::parse_from_rfc3339("2019-10-23T11:34:29.416Z").unwrap()
295        );
296    }
297
298    #[rstest]
299    fn test_parse_bar_message() {
300        let json_data = load_test_json("bar.json");
301        let message: BarMsg = serde_json::from_str(&json_data).unwrap();
302
303        assert_eq!(message.symbol, "XBTUSD");
304        assert_eq!(message.exchange, TardisExchange::Bitmex);
305        assert_eq!(message.name, "trade_bar_10000ms");
306        assert_eq!(message.interval, 10_000);
307        assert_eq!(message.open, 7_623.5);
308        assert_eq!(message.high, 7_623.5);
309        assert_eq!(message.low, 7_623.0);
310        assert_eq!(message.close, 7_623.5);
311        assert_eq!(message.volume, 37_034.0);
312        assert_eq!(message.buy_volume, 24_244.0);
313        assert_eq!(message.sell_volume, 12_790.0);
314        assert_eq!(message.trades, 9);
315        assert_eq!(message.vwap, 7_623.327320840309);
316        assert_eq!(
317            message.open_timestamp,
318            DateTime::parse_from_rfc3339("2019-10-25T13:11:31.574Z").unwrap()
319        );
320        assert_eq!(
321            message.close_timestamp,
322            DateTime::parse_from_rfc3339("2019-10-25T13:11:39.212Z").unwrap()
323        );
324        assert_eq!(
325            message.local_timestamp,
326            DateTime::parse_from_rfc3339("2019-10-25T13:11:40.369Z").unwrap()
327        );
328        assert_eq!(
329            message.timestamp,
330            DateTime::parse_from_rfc3339("2019-10-25T13:11:40.000Z").unwrap()
331        );
332    }
333
334    #[rstest]
335    fn test_parse_disconnect_message() {
336        let json_data = load_test_json("disconnect.json");
337        let message: DisconnectMsg = serde_json::from_str(&json_data).unwrap();
338
339        assert_eq!(message.exchange, TardisExchange::Deribit);
340        assert_eq!(
341            message.local_timestamp,
342            DateTime::parse_from_rfc3339("2019-10-23T11:34:29.416Z").unwrap()
343        );
344    }
345}