1use 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
44pub 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
81pub 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#[allow(dead_code)] fn 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
108pub 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 deltas.push(OrderBookDelta::clear(
127 instrument_id,
128 msg.change_id,
129 ts_event,
130 ts_init,
131 ));
132
133 for (i, bid) in msg.bids.iter().enumerate() {
135 if bid.len() >= 3 {
136 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, msg.change_id,
150 ts_event,
151 ts_init,
152 ));
153 }
154 }
155 }
156
157 let num_bids = msg.bids.len();
159 for (i, ask) in msg.asks.iter().enumerate() {
160 if ask.len() >= 3 {
161 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, msg.change_id,
175 ts_event,
176 ts_init,
177 ));
178 }
179 }
180 }
181
182 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
198pub 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 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, msg.change_id,
238 ts_event,
239 ts_init,
240 ));
241 }
242 }
243
244 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, msg.change_id,
268 ts_event,
269 ts_init,
270 ));
271 }
272 }
273
274 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
290pub 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
306pub 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
337pub 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#[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#[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#[must_use]
402pub fn parse_ticker_to_funding_rate(
403 msg: &DeribitTickerMsg,
404 instrument: &InstrumentAny,
405 ts_init: UnixNanos,
406) -> Option<FundingRateUpdate> {
407 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 Some(FundingRateUpdate::new(
416 instrument_id,
417 rate,
418 None, ts_event,
420 ts_init,
421 ))
422}
423
424#[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, ts_event,
443 ts_init,
444 ))
445}
446
447pub 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
482pub 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 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 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 assert_eq!(deltas.deltas.len(), 11);
587
588 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
590
591 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 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 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 assert_eq!(deltas.deltas.len(), 4);
626
627 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 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 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 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 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 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 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 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 assert_eq!(deltas.deltas.len(), 11);
753
754 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
756
757 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 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 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 assert_eq!(deltas.deltas.len(), 4);
812
813 assert_ne!(deltas.deltas[0].action, BookAction::Clear);
815
816 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 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 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 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 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 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()); }
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 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)); assert_eq!(bar.ts_event, UnixNanos::new(1_767_200_040_000_000_000));
983 }
984}