nautilus_deribit/websocket/
parse.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//! Parsing functions for converting Deribit WebSocket messages to Nautilus domain types.
17
18use ahash::AHashMap;
19use nautilus_core::{UnixNanos, datetime::NANOSECONDS_IN_MILLISECOND};
20use nautilus_model::{
21    data::{BookOrder, Data, OrderBookDelta, OrderBookDeltas, QuoteTick, TradeTick},
22    enums::{AggressorSide, BookAction, OrderSide, RecordFlag},
23    identifiers::TradeId,
24    instruments::{Instrument, InstrumentAny},
25    types::{Price, Quantity},
26};
27use ustr::Ustr;
28
29use super::{
30    enums::{DeribitBookAction, DeribitBookMsgType},
31    messages::{DeribitBookMsg, DeribitQuoteMsg, DeribitTickerMsg, DeribitTradeMsg},
32};
33
34/// Parses a Deribit trade message into a Nautilus `TradeTick`.
35///
36/// # Errors
37///
38/// Returns an error if the trade cannot be parsed.
39pub fn parse_trade_msg(
40    msg: &DeribitTradeMsg,
41    instrument: &InstrumentAny,
42    ts_init: UnixNanos,
43) -> anyhow::Result<TradeTick> {
44    let instrument_id = instrument.id();
45    let price_precision = instrument.price_precision();
46    let size_precision = instrument.size_precision();
47
48    let price = Price::new(msg.price, price_precision);
49    let size = Quantity::new(msg.amount.abs(), size_precision);
50
51    let aggressor_side = match msg.direction.as_str() {
52        "buy" => AggressorSide::Buyer,
53        "sell" => AggressorSide::Seller,
54        _ => AggressorSide::NoAggressor,
55    };
56
57    let trade_id = TradeId::new(&msg.trade_id);
58    let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
59
60    TradeTick::new_checked(
61        instrument_id,
62        price,
63        size,
64        aggressor_side,
65        trade_id,
66        ts_event,
67        ts_init,
68    )
69}
70
71/// Parses a vector of Deribit trade messages into Nautilus `Data` items.
72pub fn parse_trades_data(
73    trades: Vec<DeribitTradeMsg>,
74    instruments_cache: &AHashMap<Ustr, InstrumentAny>,
75    ts_init: UnixNanos,
76) -> Vec<Data> {
77    trades
78        .iter()
79        .filter_map(|msg| {
80            instruments_cache
81                .get(&msg.instrument_name)
82                .and_then(|inst| parse_trade_msg(msg, inst, ts_init).ok())
83                .map(Data::Trade)
84        })
85        .collect()
86}
87
88/// Converts a Deribit book action to Nautilus `BookAction`.
89#[allow(dead_code)] // Reserved for future structured book parsing
90fn convert_book_action(action: &DeribitBookAction) -> BookAction {
91    match action {
92        DeribitBookAction::New => BookAction::Add,
93        DeribitBookAction::Change => BookAction::Update,
94        DeribitBookAction::Delete => BookAction::Delete,
95    }
96}
97
98/// Parses a Deribit order book snapshot into Nautilus `OrderBookDeltas`.
99///
100/// # Errors
101///
102/// Returns an error if the book data cannot be parsed.
103pub fn parse_book_snapshot(
104    msg: &DeribitBookMsg,
105    instrument: &InstrumentAny,
106    ts_init: UnixNanos,
107) -> anyhow::Result<OrderBookDeltas> {
108    let instrument_id = instrument.id();
109    let price_precision = instrument.price_precision();
110    let size_precision = instrument.size_precision();
111    let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
112
113    let mut deltas = Vec::new();
114
115    // Add CLEAR action first for snapshot
116    deltas.push(OrderBookDelta::clear(
117        instrument_id,
118        msg.change_id,
119        ts_event,
120        ts_init,
121    ));
122
123    // Parse bids: ["new", price, amount] for snapshot (3-element format)
124    for (i, bid) in msg.bids.iter().enumerate() {
125        if bid.len() >= 3 {
126            // Skip action field (bid[0]), use bid[1] for price and bid[2] for amount
127            let price_val = bid[1].as_f64().unwrap_or(0.0);
128            let amount_val = bid[2].as_f64().unwrap_or(0.0);
129
130            if amount_val > 0.0 {
131                let price = Price::new(price_val, price_precision);
132                let size = Quantity::new(amount_val, size_precision);
133
134                deltas.push(OrderBookDelta::new(
135                    instrument_id,
136                    BookAction::Add,
137                    BookOrder::new(OrderSide::Buy, price, size, i as u64),
138                    0, // No flags for regular deltas
139                    msg.change_id,
140                    ts_event,
141                    ts_init,
142                ));
143            }
144        }
145    }
146
147    // Parse asks: ["new", price, amount] for snapshot (3-element format)
148    let num_bids = msg.bids.len();
149    for (i, ask) in msg.asks.iter().enumerate() {
150        if ask.len() >= 3 {
151            // Skip action field (ask[0]), use ask[1] for price and ask[2] for amount
152            let price_val = ask[1].as_f64().unwrap_or(0.0);
153            let amount_val = ask[2].as_f64().unwrap_or(0.0);
154
155            if amount_val > 0.0 {
156                let price = Price::new(price_val, price_precision);
157                let size = Quantity::new(amount_val, size_precision);
158
159                deltas.push(OrderBookDelta::new(
160                    instrument_id,
161                    BookAction::Add,
162                    BookOrder::new(OrderSide::Sell, price, size, (num_bids + i) as u64),
163                    0, // No flags for regular deltas
164                    msg.change_id,
165                    ts_event,
166                    ts_init,
167                ));
168            }
169        }
170    }
171
172    // Set F_LAST flag on the last delta
173    if let Some(last) = deltas.last_mut() {
174        *last = OrderBookDelta::new(
175            last.instrument_id,
176            last.action,
177            last.order,
178            RecordFlag::F_LAST as u8,
179            last.sequence,
180            last.ts_event,
181            last.ts_init,
182        );
183    }
184
185    Ok(OrderBookDeltas::new(instrument_id, deltas))
186}
187
188/// Parses a Deribit order book change (delta) into Nautilus `OrderBookDeltas`.
189///
190/// # Errors
191///
192/// Returns an error if the book data cannot be parsed.
193pub fn parse_book_delta(
194    msg: &DeribitBookMsg,
195    instrument: &InstrumentAny,
196    ts_init: UnixNanos,
197) -> anyhow::Result<OrderBookDeltas> {
198    let instrument_id = instrument.id();
199    let price_precision = instrument.price_precision();
200    let size_precision = instrument.size_precision();
201    let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
202
203    let mut deltas = Vec::new();
204
205    // Parse bids: [action, price, amount] for delta
206    for (i, bid) in msg.bids.iter().enumerate() {
207        if bid.len() >= 3 {
208            let action_str = bid[0].as_str().unwrap_or("new");
209            let price_val = bid[1].as_f64().unwrap_or(0.0);
210            let amount_val = bid[2].as_f64().unwrap_or(0.0);
211
212            let action = match action_str {
213                "new" => BookAction::Add,
214                "change" => BookAction::Update,
215                "delete" => BookAction::Delete,
216                _ => continue,
217            };
218
219            let price = Price::new(price_val, price_precision);
220            let size = Quantity::new(amount_val.abs(), size_precision);
221
222            deltas.push(OrderBookDelta::new(
223                instrument_id,
224                action,
225                BookOrder::new(OrderSide::Buy, price, size, i as u64),
226                0, // No flags for regular deltas
227                msg.change_id,
228                ts_event,
229                ts_init,
230            ));
231        }
232    }
233
234    // Parse asks: [action, price, amount] for delta
235    let num_bids = msg.bids.len();
236    for (i, ask) in msg.asks.iter().enumerate() {
237        if ask.len() >= 3 {
238            let action_str = ask[0].as_str().unwrap_or("new");
239            let price_val = ask[1].as_f64().unwrap_or(0.0);
240            let amount_val = ask[2].as_f64().unwrap_or(0.0);
241
242            let action = match action_str {
243                "new" => BookAction::Add,
244                "change" => BookAction::Update,
245                "delete" => BookAction::Delete,
246                _ => continue,
247            };
248
249            let price = Price::new(price_val, price_precision);
250            let size = Quantity::new(amount_val.abs(), size_precision);
251
252            deltas.push(OrderBookDelta::new(
253                instrument_id,
254                action,
255                BookOrder::new(OrderSide::Sell, price, size, (num_bids + i) as u64),
256                0, // No flags for regular deltas
257                msg.change_id,
258                ts_event,
259                ts_init,
260            ));
261        }
262    }
263
264    // Set F_LAST flag on the last delta
265    if let Some(last) = deltas.last_mut() {
266        *last = OrderBookDelta::new(
267            last.instrument_id,
268            last.action,
269            last.order,
270            RecordFlag::F_LAST as u8,
271            last.sequence,
272            last.ts_event,
273            last.ts_init,
274        );
275    }
276
277    Ok(OrderBookDeltas::new(instrument_id, deltas))
278}
279
280/// Parses a Deribit order book message (snapshot or delta) into Nautilus `OrderBookDeltas`.
281///
282/// # Errors
283///
284/// Returns an error if the book data cannot be parsed.
285pub fn parse_book_msg(
286    msg: &DeribitBookMsg,
287    instrument: &InstrumentAny,
288    ts_init: UnixNanos,
289) -> anyhow::Result<OrderBookDeltas> {
290    match msg.msg_type {
291        DeribitBookMsgType::Snapshot => parse_book_snapshot(msg, instrument, ts_init),
292        DeribitBookMsgType::Change => parse_book_delta(msg, instrument, ts_init),
293    }
294}
295
296/// Parses a Deribit ticker message into a Nautilus `QuoteTick`.
297///
298/// # Errors
299///
300/// Returns an error if the quote cannot be parsed.
301pub fn parse_ticker_to_quote(
302    msg: &DeribitTickerMsg,
303    instrument: &InstrumentAny,
304    ts_init: UnixNanos,
305) -> anyhow::Result<QuoteTick> {
306    let instrument_id = instrument.id();
307    let price_precision = instrument.price_precision();
308    let size_precision = instrument.size_precision();
309
310    let bid_price = Price::new(msg.best_bid_price.unwrap_or(0.0), price_precision);
311    let ask_price = Price::new(msg.best_ask_price.unwrap_or(0.0), price_precision);
312    let bid_size = Quantity::new(msg.best_bid_amount.unwrap_or(0.0), size_precision);
313    let ask_size = Quantity::new(msg.best_ask_amount.unwrap_or(0.0), size_precision);
314    let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
315
316    QuoteTick::new_checked(
317        instrument_id,
318        bid_price,
319        ask_price,
320        bid_size,
321        ask_size,
322        ts_event,
323        ts_init,
324    )
325}
326
327/// Parses a Deribit quote message into a Nautilus `QuoteTick`.
328///
329/// # Errors
330///
331/// Returns an error if the quote cannot be parsed.
332pub fn parse_quote_msg(
333    msg: &DeribitQuoteMsg,
334    instrument: &InstrumentAny,
335    ts_init: UnixNanos,
336) -> anyhow::Result<QuoteTick> {
337    let instrument_id = instrument.id();
338    let price_precision = instrument.price_precision();
339    let size_precision = instrument.size_precision();
340
341    let bid_price = Price::new(msg.best_bid_price, price_precision);
342    let ask_price = Price::new(msg.best_ask_price, price_precision);
343    let bid_size = Quantity::new(msg.best_bid_amount, size_precision);
344    let ask_size = Quantity::new(msg.best_ask_amount, size_precision);
345    let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
346
347    QuoteTick::new_checked(
348        instrument_id,
349        bid_price,
350        ask_price,
351        bid_size,
352        ask_size,
353        ts_event,
354        ts_init,
355    )
356}
357
358#[cfg(test)]
359mod tests {
360    use rstest::rstest;
361
362    use super::*;
363    use crate::{
364        common::{parse::parse_deribit_instrument_any, testing::load_test_json},
365        http::models::{DeribitInstrument, DeribitJsonRpcResponse},
366    };
367
368    /// Helper function to create a test instrument (BTC-PERPETUAL).
369    fn test_perpetual_instrument() -> InstrumentAny {
370        let json = load_test_json("http_get_instruments.json");
371        let response: DeribitJsonRpcResponse<Vec<DeribitInstrument>> =
372            serde_json::from_str(&json).unwrap();
373        let instrument = &response.result.unwrap()[0];
374        parse_deribit_instrument_any(instrument, UnixNanos::default(), UnixNanos::default())
375            .unwrap()
376            .unwrap()
377    }
378
379    #[rstest]
380    fn test_parse_trade_msg_sell() {
381        let instrument = test_perpetual_instrument();
382        let json = load_test_json("ws_trades.json");
383        let response: serde_json::Value = serde_json::from_str(&json).unwrap();
384        let trades: Vec<DeribitTradeMsg> =
385            serde_json::from_value(response["params"]["data"].clone()).unwrap();
386        let msg = &trades[0];
387
388        let tick = parse_trade_msg(msg, &instrument, UnixNanos::default()).unwrap();
389
390        assert_eq!(tick.instrument_id, instrument.id());
391        assert_eq!(tick.price, instrument.make_price(92294.5));
392        assert_eq!(tick.size, instrument.make_qty(10.0, None));
393        assert_eq!(tick.aggressor_side, AggressorSide::Seller);
394        assert_eq!(tick.trade_id.to_string(), "403691824");
395        assert_eq!(tick.ts_event, UnixNanos::new(1_765_531_356_452_000_000));
396    }
397
398    #[rstest]
399    fn test_parse_trade_msg_buy() {
400        let instrument = test_perpetual_instrument();
401        let json = load_test_json("ws_trades.json");
402        let response: serde_json::Value = serde_json::from_str(&json).unwrap();
403        let trades: Vec<DeribitTradeMsg> =
404            serde_json::from_value(response["params"]["data"].clone()).unwrap();
405        let msg = &trades[1];
406
407        let tick = parse_trade_msg(msg, &instrument, UnixNanos::default()).unwrap();
408
409        assert_eq!(tick.instrument_id, instrument.id());
410        assert_eq!(tick.price, instrument.make_price(92288.5));
411        assert_eq!(tick.size, instrument.make_qty(750.0, None));
412        assert_eq!(tick.aggressor_side, AggressorSide::Seller);
413        assert_eq!(tick.trade_id.to_string(), "403691825");
414    }
415
416    #[rstest]
417    fn test_parse_book_snapshot() {
418        let instrument = test_perpetual_instrument();
419        let json = load_test_json("ws_book_snapshot.json");
420        let response: serde_json::Value = serde_json::from_str(&json).unwrap();
421        let msg: DeribitBookMsg =
422            serde_json::from_value(response["params"]["data"].clone()).unwrap();
423
424        let deltas = parse_book_snapshot(&msg, &instrument, UnixNanos::default()).unwrap();
425
426        assert_eq!(deltas.instrument_id, instrument.id());
427        // Should have CLEAR + 5 bids + 5 asks = 11 deltas
428        assert_eq!(deltas.deltas.len(), 11);
429
430        // First delta should be CLEAR
431        assert_eq!(deltas.deltas[0].action, BookAction::Clear);
432
433        // Check first bid
434        let first_bid = &deltas.deltas[1];
435        assert_eq!(first_bid.action, BookAction::Add);
436        assert_eq!(first_bid.order.side, OrderSide::Buy);
437        assert_eq!(first_bid.order.price, instrument.make_price(42500.0));
438        assert_eq!(first_bid.order.size, instrument.make_qty(1000.0, None));
439
440        // Check first ask
441        let first_ask = &deltas.deltas[6];
442        assert_eq!(first_ask.action, BookAction::Add);
443        assert_eq!(first_ask.order.side, OrderSide::Sell);
444        assert_eq!(first_ask.order.price, instrument.make_price(42501.0));
445        assert_eq!(first_ask.order.size, instrument.make_qty(800.0, None));
446
447        // Check F_LAST flag on last delta
448        let last = deltas.deltas.last().unwrap();
449        assert_eq!(
450            last.flags & RecordFlag::F_LAST as u8,
451            RecordFlag::F_LAST as u8
452        );
453    }
454
455    #[rstest]
456    fn test_parse_book_delta() {
457        let instrument = test_perpetual_instrument();
458        let json = load_test_json("ws_book_delta.json");
459        let response: serde_json::Value = serde_json::from_str(&json).unwrap();
460        let msg: DeribitBookMsg =
461            serde_json::from_value(response["params"]["data"].clone()).unwrap();
462
463        let deltas = parse_book_delta(&msg, &instrument, UnixNanos::default()).unwrap();
464
465        assert_eq!(deltas.instrument_id, instrument.id());
466        // Should have 2 bid deltas + 2 ask deltas = 4 deltas
467        assert_eq!(deltas.deltas.len(), 4);
468
469        // Check first bid - "change" action
470        let bid_change = &deltas.deltas[0];
471        assert_eq!(bid_change.action, BookAction::Update);
472        assert_eq!(bid_change.order.side, OrderSide::Buy);
473        assert_eq!(bid_change.order.price, instrument.make_price(42500.0));
474        assert_eq!(bid_change.order.size, instrument.make_qty(950.0, None));
475
476        // Check second bid - "new" action
477        let bid_new = &deltas.deltas[1];
478        assert_eq!(bid_new.action, BookAction::Add);
479        assert_eq!(bid_new.order.side, OrderSide::Buy);
480        assert_eq!(bid_new.order.price, instrument.make_price(42498.5));
481        assert_eq!(bid_new.order.size, instrument.make_qty(300.0, None));
482
483        // Check first ask - "delete" action
484        let ask_delete = &deltas.deltas[2];
485        assert_eq!(ask_delete.action, BookAction::Delete);
486        assert_eq!(ask_delete.order.side, OrderSide::Sell);
487        assert_eq!(ask_delete.order.price, instrument.make_price(42501.0));
488        assert_eq!(ask_delete.order.size, instrument.make_qty(0.0, None));
489
490        // Check second ask - "change" action
491        let ask_change = &deltas.deltas[3];
492        assert_eq!(ask_change.action, BookAction::Update);
493        assert_eq!(ask_change.order.side, OrderSide::Sell);
494        assert_eq!(ask_change.order.price, instrument.make_price(42501.5));
495        assert_eq!(ask_change.order.size, instrument.make_qty(700.0, None));
496
497        // Check F_LAST flag on last delta
498        let last = deltas.deltas.last().unwrap();
499        assert_eq!(
500            last.flags & RecordFlag::F_LAST as u8,
501            RecordFlag::F_LAST as u8
502        );
503    }
504
505    #[rstest]
506    fn test_parse_ticker_to_quote() {
507        let instrument = test_perpetual_instrument();
508        let json = load_test_json("ws_ticker.json");
509        let response: serde_json::Value = serde_json::from_str(&json).unwrap();
510        let msg: DeribitTickerMsg =
511            serde_json::from_value(response["params"]["data"].clone()).unwrap();
512
513        // Verify the message was deserialized correctly
514        assert_eq!(msg.instrument_name.as_str(), "BTC-PERPETUAL");
515        assert_eq!(msg.timestamp, 1_765_541_474_086);
516        assert_eq!(msg.best_bid_price, Some(92283.5));
517        assert_eq!(msg.best_ask_price, Some(92284.0));
518        assert_eq!(msg.best_bid_amount, Some(117660.0));
519        assert_eq!(msg.best_ask_amount, Some(186520.0));
520        assert_eq!(msg.mark_price, 92281.78);
521        assert_eq!(msg.index_price, 92263.55);
522        assert_eq!(msg.open_interest, 1132329370.0);
523
524        let quote = parse_ticker_to_quote(&msg, &instrument, UnixNanos::default()).unwrap();
525
526        assert_eq!(quote.instrument_id, instrument.id());
527        assert_eq!(quote.bid_price, instrument.make_price(92283.5));
528        assert_eq!(quote.ask_price, instrument.make_price(92284.0));
529        assert_eq!(quote.bid_size, instrument.make_qty(117660.0, None));
530        assert_eq!(quote.ask_size, instrument.make_qty(186520.0, None));
531        assert_eq!(quote.ts_event, UnixNanos::new(1_765_541_474_086_000_000));
532    }
533
534    #[rstest]
535    fn test_parse_quote_msg() {
536        let instrument = test_perpetual_instrument();
537        let json = load_test_json("ws_quote.json");
538        let response: serde_json::Value = serde_json::from_str(&json).unwrap();
539        let msg: DeribitQuoteMsg =
540            serde_json::from_value(response["params"]["data"].clone()).unwrap();
541
542        // Verify the message was deserialized correctly
543        assert_eq!(msg.instrument_name.as_str(), "BTC-PERPETUAL");
544        assert_eq!(msg.timestamp, 1_765_541_767_174);
545        assert_eq!(msg.best_bid_price, 92288.0);
546        assert_eq!(msg.best_ask_price, 92288.5);
547        assert_eq!(msg.best_bid_amount, 133440.0);
548        assert_eq!(msg.best_ask_amount, 99470.0);
549
550        let quote = parse_quote_msg(&msg, &instrument, UnixNanos::default()).unwrap();
551
552        assert_eq!(quote.instrument_id, instrument.id());
553        assert_eq!(quote.bid_price, instrument.make_price(92288.0));
554        assert_eq!(quote.ask_price, instrument.make_price(92288.5));
555        assert_eq!(quote.bid_size, instrument.make_qty(133440.0, None));
556        assert_eq!(quote.ask_size, instrument.make_qty(99470.0, None));
557        assert_eq!(quote.ts_event, UnixNanos::new(1_765_541_767_174_000_000));
558    }
559
560    #[rstest]
561    fn test_parse_book_msg_snapshot() {
562        let instrument = test_perpetual_instrument();
563        let json = load_test_json("ws_book_snapshot.json");
564        let response: serde_json::Value = serde_json::from_str(&json).unwrap();
565        let msg: DeribitBookMsg =
566            serde_json::from_value(response["params"]["data"].clone()).unwrap();
567
568        // Validate raw message format - snapshots use 3-element arrays: ["new", price, amount]
569        assert_eq!(
570            msg.bids[0].len(),
571            3,
572            "Snapshot bids should have 3 elements: [action, price, amount]"
573        );
574        assert_eq!(
575            msg.bids[0][0].as_str(),
576            Some("new"),
577            "First element should be 'new' action for snapshot"
578        );
579        assert_eq!(
580            msg.asks[0].len(),
581            3,
582            "Snapshot asks should have 3 elements: [action, price, amount]"
583        );
584        assert_eq!(
585            msg.asks[0][0].as_str(),
586            Some("new"),
587            "First element should be 'new' action for snapshot"
588        );
589
590        let deltas = parse_book_msg(&msg, &instrument, UnixNanos::default()).unwrap();
591
592        assert_eq!(deltas.instrument_id, instrument.id());
593        // Should have CLEAR + 5 bids + 5 asks = 11 deltas
594        assert_eq!(deltas.deltas.len(), 11);
595
596        // First delta should be CLEAR
597        assert_eq!(deltas.deltas[0].action, BookAction::Clear);
598
599        // Verify first bid was parsed correctly from ["new", 42500.0, 1000.0]
600        let first_bid = &deltas.deltas[1];
601        assert_eq!(first_bid.action, BookAction::Add);
602        assert_eq!(first_bid.order.side, OrderSide::Buy);
603        assert_eq!(first_bid.order.price, instrument.make_price(42500.0));
604        assert_eq!(first_bid.order.size, instrument.make_qty(1000.0, None));
605
606        // Verify first ask was parsed correctly from ["new", 42501.0, 800.0]
607        let first_ask = &deltas.deltas[6];
608        assert_eq!(first_ask.action, BookAction::Add);
609        assert_eq!(first_ask.order.side, OrderSide::Sell);
610        assert_eq!(first_ask.order.price, instrument.make_price(42501.0));
611        assert_eq!(first_ask.order.size, instrument.make_qty(800.0, None));
612    }
613
614    #[rstest]
615    fn test_parse_book_msg_delta() {
616        let instrument = test_perpetual_instrument();
617        let json = load_test_json("ws_book_delta.json");
618        let response: serde_json::Value = serde_json::from_str(&json).unwrap();
619        let msg: DeribitBookMsg =
620            serde_json::from_value(response["params"]["data"].clone()).unwrap();
621
622        // Validate raw message format - deltas use 3-element arrays: [action, price, amount]
623        assert_eq!(
624            msg.bids[0].len(),
625            3,
626            "Delta bids should have 3 elements: [action, price, amount]"
627        );
628        assert_eq!(
629            msg.bids[0][0].as_str(),
630            Some("change"),
631            "First bid should be 'change' action"
632        );
633        assert_eq!(
634            msg.bids[1][0].as_str(),
635            Some("new"),
636            "Second bid should be 'new' action"
637        );
638        assert_eq!(
639            msg.asks[0].len(),
640            3,
641            "Delta asks should have 3 elements: [action, price, amount]"
642        );
643        assert_eq!(
644            msg.asks[0][0].as_str(),
645            Some("delete"),
646            "First ask should be 'delete' action"
647        );
648
649        let deltas = parse_book_msg(&msg, &instrument, UnixNanos::default()).unwrap();
650
651        assert_eq!(deltas.instrument_id, instrument.id());
652        // Should have 2 bid deltas + 2 ask deltas = 4 deltas
653        assert_eq!(deltas.deltas.len(), 4);
654
655        // Delta should not have CLEAR action
656        assert_ne!(deltas.deltas[0].action, BookAction::Clear);
657
658        // Verify first bid "change" action was parsed correctly from ["change", 42500.0, 950.0]
659        let bid_change = &deltas.deltas[0];
660        assert_eq!(bid_change.action, BookAction::Update);
661        assert_eq!(bid_change.order.side, OrderSide::Buy);
662        assert_eq!(bid_change.order.price, instrument.make_price(42500.0));
663        assert_eq!(bid_change.order.size, instrument.make_qty(950.0, None));
664
665        // Verify second bid "new" action was parsed correctly from ["new", 42498.5, 300.0]
666        let bid_new = &deltas.deltas[1];
667        assert_eq!(bid_new.action, BookAction::Add);
668        assert_eq!(bid_new.order.side, OrderSide::Buy);
669        assert_eq!(bid_new.order.price, instrument.make_price(42498.5));
670        assert_eq!(bid_new.order.size, instrument.make_qty(300.0, None));
671
672        // Verify first ask "delete" action was parsed correctly from ["delete", 42501.0, 0.0]
673        let ask_delete = &deltas.deltas[2];
674        assert_eq!(ask_delete.action, BookAction::Delete);
675        assert_eq!(ask_delete.order.side, OrderSide::Sell);
676        assert_eq!(ask_delete.order.price, instrument.make_price(42501.0));
677
678        // Verify second ask "change" action was parsed correctly from ["change", 42501.5, 700.0]
679        let ask_change = &deltas.deltas[3];
680        assert_eq!(ask_change.action, BookAction::Update);
681        assert_eq!(ask_change.order.side, OrderSide::Sell);
682        assert_eq!(ask_change.order.price, instrument.make_price(42501.5));
683        assert_eq!(ask_change.order.size, instrument.make_qty(700.0, None));
684    }
685}