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::{
22        Bar, BarType, BookOrder, Data, FundingRateUpdate, IndexPriceUpdate, MarkPriceUpdate,
23        OrderBookDelta, OrderBookDeltas, QuoteTick, TradeTick, bar::BarSpecification,
24    },
25    enums::{
26        AggregationSource, AggressorSide, BarAggregation, BookAction, OrderSide, PriceType,
27        RecordFlag,
28    },
29    identifiers::{InstrumentId, TradeId},
30    instruments::{Instrument, InstrumentAny},
31    types::{Price, Quantity},
32};
33use rust_decimal::prelude::FromPrimitive;
34use ustr::Ustr;
35
36use super::{
37    enums::{DeribitBookAction, DeribitBookMsgType},
38    messages::{
39        DeribitBookMsg, DeribitChartMsg, DeribitPerpetualMsg, DeribitQuoteMsg, DeribitTickerMsg,
40        DeribitTradeMsg,
41    },
42};
43
44/// Parses a Deribit trade message into a Nautilus `TradeTick`.
45///
46/// # Errors
47///
48/// Returns an error if the trade cannot be parsed.
49pub fn parse_trade_msg(
50    msg: &DeribitTradeMsg,
51    instrument: &InstrumentAny,
52    ts_init: UnixNanos,
53) -> anyhow::Result<TradeTick> {
54    let instrument_id = instrument.id();
55    let price_precision = instrument.price_precision();
56    let size_precision = instrument.size_precision();
57
58    let price = Price::new(msg.price, price_precision);
59    let size = Quantity::new(msg.amount.abs(), size_precision);
60
61    let aggressor_side = match msg.direction.as_str() {
62        "buy" => AggressorSide::Buyer,
63        "sell" => AggressorSide::Seller,
64        _ => AggressorSide::NoAggressor,
65    };
66
67    let trade_id = TradeId::new(&msg.trade_id);
68    let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
69
70    TradeTick::new_checked(
71        instrument_id,
72        price,
73        size,
74        aggressor_side,
75        trade_id,
76        ts_event,
77        ts_init,
78    )
79}
80
81/// Parses a vector of Deribit trade messages into Nautilus `Data` items.
82pub fn parse_trades_data(
83    trades: Vec<DeribitTradeMsg>,
84    instruments_cache: &AHashMap<Ustr, InstrumentAny>,
85    ts_init: UnixNanos,
86) -> Vec<Data> {
87    trades
88        .iter()
89        .filter_map(|msg| {
90            instruments_cache
91                .get(&msg.instrument_name)
92                .and_then(|inst| parse_trade_msg(msg, inst, ts_init).ok())
93                .map(Data::Trade)
94        })
95        .collect()
96}
97
98/// Converts a Deribit book action to Nautilus `BookAction`.
99#[allow(dead_code)] // Reserved for future structured book parsing
100fn convert_book_action(action: &DeribitBookAction) -> BookAction {
101    match action {
102        DeribitBookAction::New => BookAction::Add,
103        DeribitBookAction::Change => BookAction::Update,
104        DeribitBookAction::Delete => BookAction::Delete,
105    }
106}
107
108/// Parses a Deribit order book snapshot into Nautilus `OrderBookDeltas`.
109///
110/// # Errors
111///
112/// Returns an error if the book data cannot be parsed.
113pub fn parse_book_snapshot(
114    msg: &DeribitBookMsg,
115    instrument: &InstrumentAny,
116    ts_init: UnixNanos,
117) -> anyhow::Result<OrderBookDeltas> {
118    let instrument_id = instrument.id();
119    let price_precision = instrument.price_precision();
120    let size_precision = instrument.size_precision();
121    let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
122
123    let mut deltas = Vec::new();
124
125    // Add CLEAR action first for snapshot
126    deltas.push(OrderBookDelta::clear(
127        instrument_id,
128        msg.change_id,
129        ts_event,
130        ts_init,
131    ));
132
133    // Parse bids: ["new", price, amount] for snapshot (3-element format)
134    for (i, bid) in msg.bids.iter().enumerate() {
135        if bid.len() >= 3 {
136            // Skip action field (bid[0]), use bid[1] for price and bid[2] for amount
137            let price_val = bid[1].as_f64().unwrap_or(0.0);
138            let amount_val = bid[2].as_f64().unwrap_or(0.0);
139
140            if amount_val > 0.0 {
141                let price = Price::new(price_val, price_precision);
142                let size = Quantity::new(amount_val, size_precision);
143
144                deltas.push(OrderBookDelta::new(
145                    instrument_id,
146                    BookAction::Add,
147                    BookOrder::new(OrderSide::Buy, price, size, i as u64),
148                    0, // No flags for regular deltas
149                    msg.change_id,
150                    ts_event,
151                    ts_init,
152                ));
153            }
154        }
155    }
156
157    // Parse asks: ["new", price, amount] for snapshot (3-element format)
158    let num_bids = msg.bids.len();
159    for (i, ask) in msg.asks.iter().enumerate() {
160        if ask.len() >= 3 {
161            // Skip action field (ask[0]), use ask[1] for price and ask[2] for amount
162            let price_val = ask[1].as_f64().unwrap_or(0.0);
163            let amount_val = ask[2].as_f64().unwrap_or(0.0);
164
165            if amount_val > 0.0 {
166                let price = Price::new(price_val, price_precision);
167                let size = Quantity::new(amount_val, size_precision);
168
169                deltas.push(OrderBookDelta::new(
170                    instrument_id,
171                    BookAction::Add,
172                    BookOrder::new(OrderSide::Sell, price, size, (num_bids + i) as u64),
173                    0, // No flags for regular deltas
174                    msg.change_id,
175                    ts_event,
176                    ts_init,
177                ));
178            }
179        }
180    }
181
182    // Set F_LAST flag on the last delta
183    if let Some(last) = deltas.last_mut() {
184        *last = OrderBookDelta::new(
185            last.instrument_id,
186            last.action,
187            last.order,
188            RecordFlag::F_LAST as u8,
189            last.sequence,
190            last.ts_event,
191            last.ts_init,
192        );
193    }
194
195    Ok(OrderBookDeltas::new(instrument_id, deltas))
196}
197
198/// Parses a Deribit order book change (delta) into Nautilus `OrderBookDeltas`.
199///
200/// # Errors
201///
202/// Returns an error if the book data cannot be parsed.
203pub fn parse_book_delta(
204    msg: &DeribitBookMsg,
205    instrument: &InstrumentAny,
206    ts_init: UnixNanos,
207) -> anyhow::Result<OrderBookDeltas> {
208    let instrument_id = instrument.id();
209    let price_precision = instrument.price_precision();
210    let size_precision = instrument.size_precision();
211    let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
212
213    let mut deltas = Vec::new();
214
215    // Parse bids: [action, price, amount] for delta
216    for (i, bid) in msg.bids.iter().enumerate() {
217        if bid.len() >= 3 {
218            let action_str = bid[0].as_str().unwrap_or("new");
219            let price_val = bid[1].as_f64().unwrap_or(0.0);
220            let amount_val = bid[2].as_f64().unwrap_or(0.0);
221
222            let action = match action_str {
223                "new" => BookAction::Add,
224                "change" => BookAction::Update,
225                "delete" => BookAction::Delete,
226                _ => continue,
227            };
228
229            let price = Price::new(price_val, price_precision);
230            let size = Quantity::new(amount_val.abs(), size_precision);
231
232            deltas.push(OrderBookDelta::new(
233                instrument_id,
234                action,
235                BookOrder::new(OrderSide::Buy, price, size, i as u64),
236                0, // No flags for regular deltas
237                msg.change_id,
238                ts_event,
239                ts_init,
240            ));
241        }
242    }
243
244    // Parse asks: [action, price, amount] for delta
245    let num_bids = msg.bids.len();
246    for (i, ask) in msg.asks.iter().enumerate() {
247        if ask.len() >= 3 {
248            let action_str = ask[0].as_str().unwrap_or("new");
249            let price_val = ask[1].as_f64().unwrap_or(0.0);
250            let amount_val = ask[2].as_f64().unwrap_or(0.0);
251
252            let action = match action_str {
253                "new" => BookAction::Add,
254                "change" => BookAction::Update,
255                "delete" => BookAction::Delete,
256                _ => continue,
257            };
258
259            let price = Price::new(price_val, price_precision);
260            let size = Quantity::new(amount_val.abs(), size_precision);
261
262            deltas.push(OrderBookDelta::new(
263                instrument_id,
264                action,
265                BookOrder::new(OrderSide::Sell, price, size, (num_bids + i) as u64),
266                0, // No flags for regular deltas
267                msg.change_id,
268                ts_event,
269                ts_init,
270            ));
271        }
272    }
273
274    // Set F_LAST flag on the last delta
275    if let Some(last) = deltas.last_mut() {
276        *last = OrderBookDelta::new(
277            last.instrument_id,
278            last.action,
279            last.order,
280            RecordFlag::F_LAST as u8,
281            last.sequence,
282            last.ts_event,
283            last.ts_init,
284        );
285    }
286
287    Ok(OrderBookDeltas::new(instrument_id, deltas))
288}
289
290/// Parses a Deribit order book message (snapshot or delta) into Nautilus `OrderBookDeltas`.
291///
292/// # Errors
293///
294/// Returns an error if the book data cannot be parsed.
295pub fn parse_book_msg(
296    msg: &DeribitBookMsg,
297    instrument: &InstrumentAny,
298    ts_init: UnixNanos,
299) -> anyhow::Result<OrderBookDeltas> {
300    match msg.msg_type {
301        DeribitBookMsgType::Snapshot => parse_book_snapshot(msg, instrument, ts_init),
302        DeribitBookMsgType::Change => parse_book_delta(msg, instrument, ts_init),
303    }
304}
305
306/// Parses a Deribit ticker message into a Nautilus `QuoteTick`.
307///
308/// # Errors
309///
310/// Returns an error if the quote cannot be parsed.
311pub fn parse_ticker_to_quote(
312    msg: &DeribitTickerMsg,
313    instrument: &InstrumentAny,
314    ts_init: UnixNanos,
315) -> anyhow::Result<QuoteTick> {
316    let instrument_id = instrument.id();
317    let price_precision = instrument.price_precision();
318    let size_precision = instrument.size_precision();
319
320    let bid_price = Price::new(msg.best_bid_price.unwrap_or(0.0), price_precision);
321    let ask_price = Price::new(msg.best_ask_price.unwrap_or(0.0), price_precision);
322    let bid_size = Quantity::new(msg.best_bid_amount.unwrap_or(0.0), size_precision);
323    let ask_size = Quantity::new(msg.best_ask_amount.unwrap_or(0.0), size_precision);
324    let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
325
326    QuoteTick::new_checked(
327        instrument_id,
328        bid_price,
329        ask_price,
330        bid_size,
331        ask_size,
332        ts_event,
333        ts_init,
334    )
335}
336
337/// Parses a Deribit quote message into a Nautilus `QuoteTick`.
338///
339/// # Errors
340///
341/// Returns an error if the quote cannot be parsed.
342pub fn parse_quote_msg(
343    msg: &DeribitQuoteMsg,
344    instrument: &InstrumentAny,
345    ts_init: UnixNanos,
346) -> anyhow::Result<QuoteTick> {
347    let instrument_id = instrument.id();
348    let price_precision = instrument.price_precision();
349    let size_precision = instrument.size_precision();
350
351    let bid_price = Price::new(msg.best_bid_price, price_precision);
352    let ask_price = Price::new(msg.best_ask_price, price_precision);
353    let bid_size = Quantity::new(msg.best_bid_amount, size_precision);
354    let ask_size = Quantity::new(msg.best_ask_amount, size_precision);
355    let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
356
357    QuoteTick::new_checked(
358        instrument_id,
359        bid_price,
360        ask_price,
361        bid_size,
362        ask_size,
363        ts_event,
364        ts_init,
365    )
366}
367
368/// Parses a Deribit ticker message into a Nautilus `MarkPriceUpdate`.
369#[must_use]
370pub fn parse_ticker_to_mark_price(
371    msg: &DeribitTickerMsg,
372    instrument: &InstrumentAny,
373    ts_init: UnixNanos,
374) -> MarkPriceUpdate {
375    let instrument_id = instrument.id();
376    let price_precision = instrument.price_precision();
377    let value = Price::new(msg.mark_price, price_precision);
378    let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
379
380    MarkPriceUpdate::new(instrument_id, value, ts_event, ts_init)
381}
382
383/// Parses a Deribit ticker message into a Nautilus `IndexPriceUpdate`.
384#[must_use]
385pub fn parse_ticker_to_index_price(
386    msg: &DeribitTickerMsg,
387    instrument: &InstrumentAny,
388    ts_init: UnixNanos,
389) -> IndexPriceUpdate {
390    let instrument_id = instrument.id();
391    let price_precision = instrument.price_precision();
392    let value = Price::new(msg.index_price, price_precision);
393    let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
394
395    IndexPriceUpdate::new(instrument_id, value, ts_event, ts_init)
396}
397
398/// Parses a Deribit ticker message into a Nautilus `FundingRateUpdate`.
399///
400/// Returns `None` if the instrument is not a perpetual or the funding rate is not available.
401#[must_use]
402pub fn parse_ticker_to_funding_rate(
403    msg: &DeribitTickerMsg,
404    instrument: &InstrumentAny,
405    ts_init: UnixNanos,
406) -> Option<FundingRateUpdate> {
407    // current_funding is only available for perpetual instruments
408    let funding_rate = msg.current_funding?;
409
410    let instrument_id = instrument.id();
411    let rate = rust_decimal::Decimal::from_f64(funding_rate)?;
412    let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
413
414    // Deribit ticker doesn't include next_funding_time, set to None
415    Some(FundingRateUpdate::new(
416        instrument_id,
417        rate,
418        None, // next_funding_ns not available in ticker
419        ts_event,
420        ts_init,
421    ))
422}
423
424/// Parses a Deribit perpetual channel message into a Nautilus `FundingRateUpdate`.
425///
426/// The perpetual channel (`perpetual.{instrument}.{interval}`) provides dedicated
427/// funding rate updates with the `interest` field representing the current funding rate.
428#[must_use]
429pub fn parse_perpetual_to_funding_rate(
430    msg: &DeribitPerpetualMsg,
431    instrument: &InstrumentAny,
432    ts_init: UnixNanos,
433) -> Option<FundingRateUpdate> {
434    let instrument_id = instrument.id();
435    let rate = rust_decimal::Decimal::from_f64(msg.interest)?;
436    let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
437
438    Some(FundingRateUpdate::new(
439        instrument_id,
440        rate,
441        None, // next_funding_ns not available in perpetual channel
442        ts_event,
443        ts_init,
444    ))
445}
446
447/// Converts a Deribit chart resolution and instrument to a Nautilus BarType.
448///
449/// Deribit resolutions: "1", "3", "5", "10", "15", "30", "60", "120", "180", "360", "720", "1D"
450///
451/// # Errors
452///
453/// Returns an error if the resolution string is invalid or BarType construction fails.
454pub fn resolution_to_bar_type(
455    instrument_id: InstrumentId,
456    resolution: &str,
457) -> anyhow::Result<BarType> {
458    let (step, aggregation) = match resolution {
459        "1" => (1, BarAggregation::Minute),
460        "3" => (3, BarAggregation::Minute),
461        "5" => (5, BarAggregation::Minute),
462        "10" => (10, BarAggregation::Minute),
463        "15" => (15, BarAggregation::Minute),
464        "30" => (30, BarAggregation::Minute),
465        "60" => (60, BarAggregation::Minute),
466        "120" => (120, BarAggregation::Minute),
467        "180" => (180, BarAggregation::Minute),
468        "360" => (360, BarAggregation::Minute),
469        "720" => (720, BarAggregation::Minute),
470        "1D" => (1, BarAggregation::Day),
471        _ => anyhow::bail!("Unsupported Deribit resolution: {resolution}"),
472    };
473
474    let spec = BarSpecification::new(step, aggregation, PriceType::Last);
475    Ok(BarType::new(
476        instrument_id,
477        spec,
478        AggregationSource::External,
479    ))
480}
481
482/// Parses a Deribit chart message from a WebSocket subscription into a [`Bar`].
483///
484/// Converts a single OHLCV data point from the `chart.trades.{instrument}.{resolution}` channel
485/// into a Nautilus Bar object.
486///
487/// # Errors
488///
489/// Returns an error if:
490/// - Price or volume values are invalid
491/// - Bar construction fails validation
492pub fn parse_chart_msg(
493    chart_msg: &DeribitChartMsg,
494    bar_type: BarType,
495    price_precision: u8,
496    size_precision: u8,
497    ts_init: UnixNanos,
498) -> anyhow::Result<Bar> {
499    use anyhow::Context;
500
501    let open = Price::new_checked(chart_msg.open, price_precision).context("Invalid open price")?;
502    let high = Price::new_checked(chart_msg.high, price_precision).context("Invalid high price")?;
503    let low = Price::new_checked(chart_msg.low, price_precision).context("Invalid low price")?;
504    let close =
505        Price::new_checked(chart_msg.close, price_precision).context("Invalid close price")?;
506    let volume =
507        Quantity::new_checked(chart_msg.volume, size_precision).context("Invalid volume")?;
508
509    // Convert timestamp from milliseconds to nanoseconds
510    let ts_event = UnixNanos::from(chart_msg.tick * NANOSECONDS_IN_MILLISECOND);
511
512    Bar::new_checked(bar_type, open, high, low, close, volume, ts_event, ts_init)
513        .context("Invalid OHLC bar")
514}
515
516#[cfg(test)]
517mod tests {
518    use rstest::rstest;
519
520    use super::*;
521    use crate::{
522        common::{parse::parse_deribit_instrument_any, testing::load_test_json},
523        http::models::{DeribitInstrument, DeribitJsonRpcResponse},
524    };
525
526    /// Helper function to create a test instrument (BTC-PERPETUAL).
527    fn test_perpetual_instrument() -> InstrumentAny {
528        let json = load_test_json("http_get_instruments.json");
529        let response: DeribitJsonRpcResponse<Vec<DeribitInstrument>> =
530            serde_json::from_str(&json).unwrap();
531        let instrument = &response.result.unwrap()[0];
532        parse_deribit_instrument_any(instrument, UnixNanos::default(), UnixNanos::default())
533            .unwrap()
534            .unwrap()
535    }
536
537    #[rstest]
538    fn test_parse_trade_msg_sell() {
539        let instrument = test_perpetual_instrument();
540        let json = load_test_json("ws_trades.json");
541        let response: serde_json::Value = serde_json::from_str(&json).unwrap();
542        let trades: Vec<DeribitTradeMsg> =
543            serde_json::from_value(response["params"]["data"].clone()).unwrap();
544        let msg = &trades[0];
545
546        let tick = parse_trade_msg(msg, &instrument, UnixNanos::default()).unwrap();
547
548        assert_eq!(tick.instrument_id, instrument.id());
549        assert_eq!(tick.price, instrument.make_price(92294.5));
550        assert_eq!(tick.size, instrument.make_qty(10.0, None));
551        assert_eq!(tick.aggressor_side, AggressorSide::Seller);
552        assert_eq!(tick.trade_id.to_string(), "403691824");
553        assert_eq!(tick.ts_event, UnixNanos::new(1_765_531_356_452_000_000));
554    }
555
556    #[rstest]
557    fn test_parse_trade_msg_buy() {
558        let instrument = test_perpetual_instrument();
559        let json = load_test_json("ws_trades.json");
560        let response: serde_json::Value = serde_json::from_str(&json).unwrap();
561        let trades: Vec<DeribitTradeMsg> =
562            serde_json::from_value(response["params"]["data"].clone()).unwrap();
563        let msg = &trades[1];
564
565        let tick = parse_trade_msg(msg, &instrument, UnixNanos::default()).unwrap();
566
567        assert_eq!(tick.instrument_id, instrument.id());
568        assert_eq!(tick.price, instrument.make_price(92288.5));
569        assert_eq!(tick.size, instrument.make_qty(750.0, None));
570        assert_eq!(tick.aggressor_side, AggressorSide::Seller);
571        assert_eq!(tick.trade_id.to_string(), "403691825");
572    }
573
574    #[rstest]
575    fn test_parse_book_snapshot() {
576        let instrument = test_perpetual_instrument();
577        let json = load_test_json("ws_book_snapshot.json");
578        let response: serde_json::Value = serde_json::from_str(&json).unwrap();
579        let msg: DeribitBookMsg =
580            serde_json::from_value(response["params"]["data"].clone()).unwrap();
581
582        let deltas = parse_book_snapshot(&msg, &instrument, UnixNanos::default()).unwrap();
583
584        assert_eq!(deltas.instrument_id, instrument.id());
585        // Should have CLEAR + 5 bids + 5 asks = 11 deltas
586        assert_eq!(deltas.deltas.len(), 11);
587
588        // First delta should be CLEAR
589        assert_eq!(deltas.deltas[0].action, BookAction::Clear);
590
591        // Check first bid
592        let first_bid = &deltas.deltas[1];
593        assert_eq!(first_bid.action, BookAction::Add);
594        assert_eq!(first_bid.order.side, OrderSide::Buy);
595        assert_eq!(first_bid.order.price, instrument.make_price(42500.0));
596        assert_eq!(first_bid.order.size, instrument.make_qty(1000.0, None));
597
598        // Check first ask
599        let first_ask = &deltas.deltas[6];
600        assert_eq!(first_ask.action, BookAction::Add);
601        assert_eq!(first_ask.order.side, OrderSide::Sell);
602        assert_eq!(first_ask.order.price, instrument.make_price(42501.0));
603        assert_eq!(first_ask.order.size, instrument.make_qty(800.0, None));
604
605        // Check F_LAST flag on last delta
606        let last = deltas.deltas.last().unwrap();
607        assert_eq!(
608            last.flags & RecordFlag::F_LAST as u8,
609            RecordFlag::F_LAST as u8
610        );
611    }
612
613    #[rstest]
614    fn test_parse_book_delta() {
615        let instrument = test_perpetual_instrument();
616        let json = load_test_json("ws_book_delta.json");
617        let response: serde_json::Value = serde_json::from_str(&json).unwrap();
618        let msg: DeribitBookMsg =
619            serde_json::from_value(response["params"]["data"].clone()).unwrap();
620
621        let deltas = parse_book_delta(&msg, &instrument, UnixNanos::default()).unwrap();
622
623        assert_eq!(deltas.instrument_id, instrument.id());
624        // Should have 2 bid deltas + 2 ask deltas = 4 deltas
625        assert_eq!(deltas.deltas.len(), 4);
626
627        // Check first bid - "change" action
628        let bid_change = &deltas.deltas[0];
629        assert_eq!(bid_change.action, BookAction::Update);
630        assert_eq!(bid_change.order.side, OrderSide::Buy);
631        assert_eq!(bid_change.order.price, instrument.make_price(42500.0));
632        assert_eq!(bid_change.order.size, instrument.make_qty(950.0, None));
633
634        // Check second bid - "new" action
635        let bid_new = &deltas.deltas[1];
636        assert_eq!(bid_new.action, BookAction::Add);
637        assert_eq!(bid_new.order.side, OrderSide::Buy);
638        assert_eq!(bid_new.order.price, instrument.make_price(42498.5));
639        assert_eq!(bid_new.order.size, instrument.make_qty(300.0, None));
640
641        // Check first ask - "delete" action
642        let ask_delete = &deltas.deltas[2];
643        assert_eq!(ask_delete.action, BookAction::Delete);
644        assert_eq!(ask_delete.order.side, OrderSide::Sell);
645        assert_eq!(ask_delete.order.price, instrument.make_price(42501.0));
646        assert_eq!(ask_delete.order.size, instrument.make_qty(0.0, None));
647
648        // Check second ask - "change" action
649        let ask_change = &deltas.deltas[3];
650        assert_eq!(ask_change.action, BookAction::Update);
651        assert_eq!(ask_change.order.side, OrderSide::Sell);
652        assert_eq!(ask_change.order.price, instrument.make_price(42501.5));
653        assert_eq!(ask_change.order.size, instrument.make_qty(700.0, None));
654
655        // Check F_LAST flag on last delta
656        let last = deltas.deltas.last().unwrap();
657        assert_eq!(
658            last.flags & RecordFlag::F_LAST as u8,
659            RecordFlag::F_LAST as u8
660        );
661    }
662
663    #[rstest]
664    fn test_parse_ticker_to_quote() {
665        let instrument = test_perpetual_instrument();
666        let json = load_test_json("ws_ticker.json");
667        let response: serde_json::Value = serde_json::from_str(&json).unwrap();
668        let msg: DeribitTickerMsg =
669            serde_json::from_value(response["params"]["data"].clone()).unwrap();
670
671        // Verify the message was deserialized correctly
672        assert_eq!(msg.instrument_name.as_str(), "BTC-PERPETUAL");
673        assert_eq!(msg.timestamp, 1_765_541_474_086);
674        assert_eq!(msg.best_bid_price, Some(92283.5));
675        assert_eq!(msg.best_ask_price, Some(92284.0));
676        assert_eq!(msg.best_bid_amount, Some(117660.0));
677        assert_eq!(msg.best_ask_amount, Some(186520.0));
678        assert_eq!(msg.mark_price, 92281.78);
679        assert_eq!(msg.index_price, 92263.55);
680        assert_eq!(msg.open_interest, 1132329370.0);
681
682        let quote = parse_ticker_to_quote(&msg, &instrument, UnixNanos::default()).unwrap();
683
684        assert_eq!(quote.instrument_id, instrument.id());
685        assert_eq!(quote.bid_price, instrument.make_price(92283.5));
686        assert_eq!(quote.ask_price, instrument.make_price(92284.0));
687        assert_eq!(quote.bid_size, instrument.make_qty(117660.0, None));
688        assert_eq!(quote.ask_size, instrument.make_qty(186520.0, None));
689        assert_eq!(quote.ts_event, UnixNanos::new(1_765_541_474_086_000_000));
690    }
691
692    #[rstest]
693    fn test_parse_quote_msg() {
694        let instrument = test_perpetual_instrument();
695        let json = load_test_json("ws_quote.json");
696        let response: serde_json::Value = serde_json::from_str(&json).unwrap();
697        let msg: DeribitQuoteMsg =
698            serde_json::from_value(response["params"]["data"].clone()).unwrap();
699
700        // Verify the message was deserialized correctly
701        assert_eq!(msg.instrument_name.as_str(), "BTC-PERPETUAL");
702        assert_eq!(msg.timestamp, 1_765_541_767_174);
703        assert_eq!(msg.best_bid_price, 92288.0);
704        assert_eq!(msg.best_ask_price, 92288.5);
705        assert_eq!(msg.best_bid_amount, 133440.0);
706        assert_eq!(msg.best_ask_amount, 99470.0);
707
708        let quote = parse_quote_msg(&msg, &instrument, UnixNanos::default()).unwrap();
709
710        assert_eq!(quote.instrument_id, instrument.id());
711        assert_eq!(quote.bid_price, instrument.make_price(92288.0));
712        assert_eq!(quote.ask_price, instrument.make_price(92288.5));
713        assert_eq!(quote.bid_size, instrument.make_qty(133440.0, None));
714        assert_eq!(quote.ask_size, instrument.make_qty(99470.0, None));
715        assert_eq!(quote.ts_event, UnixNanos::new(1_765_541_767_174_000_000));
716    }
717
718    #[rstest]
719    fn test_parse_book_msg_snapshot() {
720        let instrument = test_perpetual_instrument();
721        let json = load_test_json("ws_book_snapshot.json");
722        let response: serde_json::Value = serde_json::from_str(&json).unwrap();
723        let msg: DeribitBookMsg =
724            serde_json::from_value(response["params"]["data"].clone()).unwrap();
725
726        // Validate raw message format - snapshots use 3-element arrays: ["new", price, amount]
727        assert_eq!(
728            msg.bids[0].len(),
729            3,
730            "Snapshot bids should have 3 elements: [action, price, amount]"
731        );
732        assert_eq!(
733            msg.bids[0][0].as_str(),
734            Some("new"),
735            "First element should be 'new' action for snapshot"
736        );
737        assert_eq!(
738            msg.asks[0].len(),
739            3,
740            "Snapshot asks should have 3 elements: [action, price, amount]"
741        );
742        assert_eq!(
743            msg.asks[0][0].as_str(),
744            Some("new"),
745            "First element should be 'new' action for snapshot"
746        );
747
748        let deltas = parse_book_msg(&msg, &instrument, UnixNanos::default()).unwrap();
749
750        assert_eq!(deltas.instrument_id, instrument.id());
751        // Should have CLEAR + 5 bids + 5 asks = 11 deltas
752        assert_eq!(deltas.deltas.len(), 11);
753
754        // First delta should be CLEAR
755        assert_eq!(deltas.deltas[0].action, BookAction::Clear);
756
757        // Verify first bid was parsed correctly from ["new", 42500.0, 1000.0]
758        let first_bid = &deltas.deltas[1];
759        assert_eq!(first_bid.action, BookAction::Add);
760        assert_eq!(first_bid.order.side, OrderSide::Buy);
761        assert_eq!(first_bid.order.price, instrument.make_price(42500.0));
762        assert_eq!(first_bid.order.size, instrument.make_qty(1000.0, None));
763
764        // Verify first ask was parsed correctly from ["new", 42501.0, 800.0]
765        let first_ask = &deltas.deltas[6];
766        assert_eq!(first_ask.action, BookAction::Add);
767        assert_eq!(first_ask.order.side, OrderSide::Sell);
768        assert_eq!(first_ask.order.price, instrument.make_price(42501.0));
769        assert_eq!(first_ask.order.size, instrument.make_qty(800.0, None));
770    }
771
772    #[rstest]
773    fn test_parse_book_msg_delta() {
774        let instrument = test_perpetual_instrument();
775        let json = load_test_json("ws_book_delta.json");
776        let response: serde_json::Value = serde_json::from_str(&json).unwrap();
777        let msg: DeribitBookMsg =
778            serde_json::from_value(response["params"]["data"].clone()).unwrap();
779
780        // Validate raw message format - deltas use 3-element arrays: [action, price, amount]
781        assert_eq!(
782            msg.bids[0].len(),
783            3,
784            "Delta bids should have 3 elements: [action, price, amount]"
785        );
786        assert_eq!(
787            msg.bids[0][0].as_str(),
788            Some("change"),
789            "First bid should be 'change' action"
790        );
791        assert_eq!(
792            msg.bids[1][0].as_str(),
793            Some("new"),
794            "Second bid should be 'new' action"
795        );
796        assert_eq!(
797            msg.asks[0].len(),
798            3,
799            "Delta asks should have 3 elements: [action, price, amount]"
800        );
801        assert_eq!(
802            msg.asks[0][0].as_str(),
803            Some("delete"),
804            "First ask should be 'delete' action"
805        );
806
807        let deltas = parse_book_msg(&msg, &instrument, UnixNanos::default()).unwrap();
808
809        assert_eq!(deltas.instrument_id, instrument.id());
810        // Should have 2 bid deltas + 2 ask deltas = 4 deltas
811        assert_eq!(deltas.deltas.len(), 4);
812
813        // Delta should not have CLEAR action
814        assert_ne!(deltas.deltas[0].action, BookAction::Clear);
815
816        // Verify first bid "change" action was parsed correctly from ["change", 42500.0, 950.0]
817        let bid_change = &deltas.deltas[0];
818        assert_eq!(bid_change.action, BookAction::Update);
819        assert_eq!(bid_change.order.side, OrderSide::Buy);
820        assert_eq!(bid_change.order.price, instrument.make_price(42500.0));
821        assert_eq!(bid_change.order.size, instrument.make_qty(950.0, None));
822
823        // Verify second bid "new" action was parsed correctly from ["new", 42498.5, 300.0]
824        let bid_new = &deltas.deltas[1];
825        assert_eq!(bid_new.action, BookAction::Add);
826        assert_eq!(bid_new.order.side, OrderSide::Buy);
827        assert_eq!(bid_new.order.price, instrument.make_price(42498.5));
828        assert_eq!(bid_new.order.size, instrument.make_qty(300.0, None));
829
830        // Verify first ask "delete" action was parsed correctly from ["delete", 42501.0, 0.0]
831        let ask_delete = &deltas.deltas[2];
832        assert_eq!(ask_delete.action, BookAction::Delete);
833        assert_eq!(ask_delete.order.side, OrderSide::Sell);
834        assert_eq!(ask_delete.order.price, instrument.make_price(42501.0));
835
836        // Verify second ask "change" action was parsed correctly from ["change", 42501.5, 700.0]
837        let ask_change = &deltas.deltas[3];
838        assert_eq!(ask_change.action, BookAction::Update);
839        assert_eq!(ask_change.order.side, OrderSide::Sell);
840        assert_eq!(ask_change.order.price, instrument.make_price(42501.5));
841        assert_eq!(ask_change.order.size, instrument.make_qty(700.0, None));
842    }
843
844    #[rstest]
845    fn test_parse_ticker_to_mark_price() {
846        let instrument = test_perpetual_instrument();
847        let json = load_test_json("ws_ticker.json");
848        let response: serde_json::Value = serde_json::from_str(&json).unwrap();
849        let msg: DeribitTickerMsg =
850            serde_json::from_value(response["params"]["data"].clone()).unwrap();
851
852        let mark_price = parse_ticker_to_mark_price(&msg, &instrument, UnixNanos::default());
853
854        assert_eq!(mark_price.instrument_id, instrument.id());
855        assert_eq!(mark_price.value, instrument.make_price(92281.78));
856        assert_eq!(
857            mark_price.ts_event,
858            UnixNanos::new(1_765_541_474_086_000_000)
859        );
860    }
861
862    #[rstest]
863    fn test_parse_ticker_to_index_price() {
864        let instrument = test_perpetual_instrument();
865        let json = load_test_json("ws_ticker.json");
866        let response: serde_json::Value = serde_json::from_str(&json).unwrap();
867        let msg: DeribitTickerMsg =
868            serde_json::from_value(response["params"]["data"].clone()).unwrap();
869
870        let index_price = parse_ticker_to_index_price(&msg, &instrument, UnixNanos::default());
871
872        assert_eq!(index_price.instrument_id, instrument.id());
873        assert_eq!(index_price.value, instrument.make_price(92263.55));
874        assert_eq!(
875            index_price.ts_event,
876            UnixNanos::new(1_765_541_474_086_000_000)
877        );
878    }
879
880    #[rstest]
881    fn test_parse_ticker_to_funding_rate() {
882        let instrument = test_perpetual_instrument();
883        let json = load_test_json("ws_ticker.json");
884        let response: serde_json::Value = serde_json::from_str(&json).unwrap();
885        let msg: DeribitTickerMsg =
886            serde_json::from_value(response["params"]["data"].clone()).unwrap();
887
888        // Verify current_funding exists in the message
889        assert!(msg.current_funding.is_some());
890
891        let funding_rate =
892            parse_ticker_to_funding_rate(&msg, &instrument, UnixNanos::default()).unwrap();
893
894        assert_eq!(funding_rate.instrument_id, instrument.id());
895        // The test fixture has current_funding value
896        assert_eq!(
897            funding_rate.ts_event,
898            UnixNanos::new(1_765_541_474_086_000_000)
899        );
900        assert!(funding_rate.next_funding_ns.is_none()); // Not available in ticker
901    }
902
903    #[rstest]
904    fn test_resolution_to_bar_type_1_minute() {
905        let instrument_id = InstrumentId::from("BTC-PERPETUAL.DERIBIT");
906        let bar_type = resolution_to_bar_type(instrument_id, "1").unwrap();
907
908        assert_eq!(bar_type.instrument_id(), instrument_id);
909        assert_eq!(bar_type.spec().step.get(), 1);
910        assert_eq!(bar_type.spec().aggregation, BarAggregation::Minute);
911        assert_eq!(bar_type.spec().price_type, PriceType::Last);
912        assert_eq!(bar_type.aggregation_source(), AggregationSource::External);
913    }
914
915    #[rstest]
916    fn test_resolution_to_bar_type_60_minute() {
917        let instrument_id = InstrumentId::from("ETH-PERPETUAL.DERIBIT");
918        let bar_type = resolution_to_bar_type(instrument_id, "60").unwrap();
919
920        assert_eq!(bar_type.instrument_id(), instrument_id);
921        assert_eq!(bar_type.spec().step.get(), 60);
922        assert_eq!(bar_type.spec().aggregation, BarAggregation::Minute);
923    }
924
925    #[rstest]
926    fn test_resolution_to_bar_type_daily() {
927        let instrument_id = InstrumentId::from("BTC-PERPETUAL.DERIBIT");
928        let bar_type = resolution_to_bar_type(instrument_id, "1D").unwrap();
929
930        assert_eq!(bar_type.instrument_id(), instrument_id);
931        assert_eq!(bar_type.spec().step.get(), 1);
932        assert_eq!(bar_type.spec().aggregation, BarAggregation::Day);
933    }
934
935    #[rstest]
936    fn test_resolution_to_bar_type_invalid() {
937        let instrument_id = InstrumentId::from("BTC-PERPETUAL.DERIBIT");
938        let result = resolution_to_bar_type(instrument_id, "invalid");
939
940        assert!(result.is_err());
941        assert!(
942            result
943                .unwrap_err()
944                .to_string()
945                .contains("Unsupported Deribit resolution")
946        );
947    }
948
949    #[rstest]
950    fn test_parse_chart_msg() {
951        let instrument = test_perpetual_instrument();
952        let json = load_test_json("ws_chart.json");
953        let response: serde_json::Value = serde_json::from_str(&json).unwrap();
954        let chart_msg: DeribitChartMsg =
955            serde_json::from_value(response["params"]["data"].clone()).unwrap();
956
957        // Verify chart message was deserialized correctly
958        assert_eq!(chart_msg.tick, 1_767_200_040_000);
959        assert_eq!(chart_msg.open, 87490.0);
960        assert_eq!(chart_msg.high, 87500.0);
961        assert_eq!(chart_msg.low, 87465.0);
962        assert_eq!(chart_msg.close, 87474.0);
963        assert_eq!(chart_msg.volume, 0.95978896);
964        assert_eq!(chart_msg.cost, 83970.0);
965
966        let bar_type = resolution_to_bar_type(instrument.id(), "1").unwrap();
967        let bar = parse_chart_msg(
968            &chart_msg,
969            bar_type,
970            instrument.price_precision(),
971            instrument.size_precision(),
972            UnixNanos::default(),
973        )
974        .unwrap();
975
976        assert_eq!(bar.bar_type, bar_type);
977        assert_eq!(bar.open, instrument.make_price(87490.0));
978        assert_eq!(bar.high, instrument.make_price(87500.0));
979        assert_eq!(bar.low, instrument.make_price(87465.0));
980        assert_eq!(bar.close, instrument.make_price(87474.0));
981        assert_eq!(bar.volume, instrument.make_qty(1.0, None)); // Rounded to 1.0 with size_precision=0
982        assert_eq!(bar.ts_event, UnixNanos::new(1_767_200_040_000_000_000));
983    }
984}