1use ahash::AHashMap;
19use anyhow::Context;
20use jiff::{Span, Timestamp, tz::Offset};
21use nautilus_core::{
22 UUID4, UnixNanos, datetime::NANOSECONDS_IN_MILLISECOND,
23 serialization::deserialize_decimal_token_borrowed,
24};
25use nautilus_model::{
26 data::{
27 Bar, BarType, BookOrder, Data, FundingRateUpdate, IndexPriceUpdate, MarkPriceUpdate,
28 OrderBookDelta, OrderBookDeltas, OrderBookDepth, QuoteTick, TradeTick,
29 bar::BarSpecification, option_chain::OptionGreeks,
30 },
31 enums::{
32 AggregationSource, AggressorSide, BarAggregation, BookAction, GreeksConvention,
33 LiquiditySide, OrderSide, OrderStatus, OrderType, PositionSide, PriceType, RecordFlag,
34 TimeInForce,
35 },
36 events::{OrderAccepted, OrderCanceled, OrderExpired, OrderUpdated},
37 identifiers::{
38 AccountId, ClientOrderId, InstrumentId, StrategyId, TradeId, TraderId, VenueOrderId,
39 },
40 instruments::{Instrument, InstrumentAny},
41 reports::{FillReport, OrderStatusReport, PositionStatusReport},
42 types::{Currency, Money, Price, Quantity},
43};
44use rust_decimal::{Decimal, prelude::ToPrimitive};
45use serde_json::value::RawValue;
46use ustr::Ustr;
47
48use super::{
49 enums::{DeribitBookAction, DeribitBookMsgType},
50 messages::{
51 DeribitBookMsg, DeribitChartMsg, DeribitOrderMsg, DeribitPerpetualMsg, DeribitQuoteMsg,
52 DeribitTickerMsg, DeribitTradeMsg, DeribitUserTradeMsg,
53 },
54};
55use crate::{common::parse::build_public_trade_id, http::models::DeribitPosition};
56
57fn next_8_utc(from_ns: UnixNanos) -> anyhow::Result<UnixNanos> {
58 let from_secs = from_ns.as_u64() / 1_000_000_000;
59 let timestamp = Timestamp::from_second(i64::try_from(from_secs)?)?;
60 let dt = Offset::UTC.to_datetime(timestamp);
61 let date = if dt.hour() < 8 {
62 dt.date()
63 } else {
64 dt.date().checked_add(Span::new().days(1))?
65 };
66 let next_8 = Offset::UTC.to_timestamp(date.at(8, 0, 0, 0))?;
67 let nanos = u64::try_from(next_8.as_nanosecond())
68 .context("GTD expiry timestamp out of UnixNanos range")?;
69 Ok(UnixNanos::from(nanos))
70}
71
72pub fn parse_trade_msg(
78 msg: &DeribitTradeMsg,
79 instrument: &InstrumentAny,
80 ts_init: UnixNanos,
81) -> anyhow::Result<TradeTick> {
82 let instrument_id = instrument.id();
83 let price_precision = instrument.price_precision();
84 let size_precision = instrument.size_precision();
85
86 let price = Price::from_decimal_dp(msg.price, price_precision)?;
87 let size = Quantity::from_decimal_dp(msg.amount.abs(), size_precision)?;
88
89 let aggressor_side = match msg.direction.as_str() {
90 "buy" => AggressorSide::Buy,
91 "sell" => AggressorSide::Sell,
92 _ => AggressorSide::NoAggressor,
93 };
94
95 let trade_id = build_public_trade_id(
96 &msg.trade_id,
97 msg.block_rfq_id,
98 msg.block_trade_id.as_deref(),
99 msg.combo_id.as_deref(),
100 );
101 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
102
103 TradeTick::new_checked(
104 instrument_id,
105 price,
106 size,
107 aggressor_side,
108 trade_id,
109 ts_event,
110 ts_init,
111 )
112}
113
114pub fn parse_trades_data(
116 trades: &[DeribitTradeMsg],
117 instruments_cache: &AHashMap<Ustr, InstrumentAny>,
118 ts_init: UnixNanos,
119) -> Vec<Data> {
120 trades
121 .iter()
122 .filter_map(|msg| {
123 instruments_cache
124 .get(&msg.instrument_name)
125 .and_then(|inst| parse_trade_msg(msg, inst, ts_init).ok())
126 .map(Data::Trade)
127 })
128 .collect()
129}
130
131fn parse_snapshot_level(
132 level: &[&RawValue],
133 index: usize,
134 side: &str,
135 instrument_name: &str,
136) -> Option<(Decimal, Decimal)> {
137 let (price_val, amount_val) = if level.len() >= 3 {
138 let price = deserialize_decimal_token_borrowed(level[1])
139 .ok()
140 .or_else(|| {
141 log::warn!(
142 "Failed to parse {side} price at index {index} for {instrument_name}: {level:?}"
143 );
144 None
145 })?;
146
147 let amount = deserialize_decimal_token_borrowed(level[2]).ok().or_else(|| {
148 log::warn!(
149 "Failed to parse {side} amount at index {index} for {instrument_name}: {level:?}"
150 );
151 None
152 })?;
153 (price, amount)
154 } else if level.len() >= 2 {
155 let price = deserialize_decimal_token_borrowed(level[0])
156 .ok()
157 .or_else(|| {
158 log::warn!(
159 "Failed to parse {side} price at index {index} for {instrument_name}: {level:?}"
160 );
161 None
162 })?;
163
164 let amount = deserialize_decimal_token_borrowed(level[1]).ok().or_else(|| {
165 log::warn!(
166 "Failed to parse {side} amount at index {index} for {instrument_name}: {level:?}"
167 );
168 None
169 })?;
170 (price, amount)
171 } else {
172 log::warn!(
173 "Invalid {side} format at index {index} for {instrument_name}: expected 2-3 elements, was {}",
174 level.len()
175 );
176 return None;
177 };
178
179 if price_val <= Decimal::ZERO {
180 log::warn!(
181 "Invalid {side} price {price_val} at index {index} for {instrument_name}: {level:?}"
182 );
183 return None;
184 }
185
186 Some((price_val, amount_val))
187}
188
189fn parse_delta_level(
190 level: &[&RawValue],
191 index: usize,
192 side: &str,
193 instrument_name: &str,
194) -> Option<(BookAction, Decimal, Decimal)> {
195 if level.len() < 3 {
196 log::warn!(
197 "Invalid {side} delta format at index {index} for {instrument_name}: expected 3 elements, was {}",
198 level.len()
199 );
200 return None;
201 }
202
203 let action_str = serde_json::from_str::<&str>(level[0].get())
204 .ok()
205 .or_else(|| {
206 log::warn!(
207 "Failed to parse {side} action at index {index} for {instrument_name}: {level:?}"
208 );
209 None
210 })?;
211
212 let deribit_action: DeribitBookAction = action_str.parse().ok().or_else(|| {
213 log::warn!(
214 "Unknown {side} action '{action_str}' at index {index} for {instrument_name}: {level:?}"
215 );
216 None
217 })?;
218
219 let price_val = deserialize_decimal_token_borrowed(level[1])
220 .ok()
221 .or_else(|| {
222 log::warn!(
223 "Failed to parse {side} price at index {index} for {instrument_name}: {level:?}"
224 );
225 None
226 })?;
227
228 let amount_val = deserialize_decimal_token_borrowed(level[2])
229 .ok()
230 .or_else(|| {
231 log::warn!(
232 "Failed to parse {side} amount at index {index} for {instrument_name}: {level:?}"
233 );
234 None
235 })?;
236
237 if price_val <= Decimal::ZERO {
238 log::warn!(
239 "Invalid {side} price {price_val} at index {index} for {instrument_name}: {level:?}"
240 );
241 return None;
242 }
243
244 Some((deribit_action.into(), price_val, amount_val))
245}
246
247pub fn parse_book_snapshot(
257 msg: &DeribitBookMsg<'_>,
258 instrument: &InstrumentAny,
259 ts_init: UnixNanos,
260) -> anyhow::Result<OrderBookDeltas> {
261 let instrument_id = instrument.id();
262 let price_precision = instrument.price_precision();
263 let size_precision = instrument.size_precision();
264 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
265
266 let mut deltas = Vec::new();
267
268 let has_levels = !msg.bids.is_empty() || !msg.asks.is_empty();
269
270 let clear_flags = if has_levels {
272 RecordFlag::F_SNAPSHOT as u8
273 } else {
274 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
275 };
276
277 deltas.push(OrderBookDelta::new(
278 instrument_id,
279 BookAction::Clear,
280 BookOrder::default(),
281 clear_flags,
282 msg.change_id,
283 ts_event,
284 ts_init,
285 ));
286
287 let sides = [
288 (OrderSide::Buy, "bid", &msg.bids, 0),
289 (OrderSide::Sell, "ask", &msg.asks, msg.bids.len()),
290 ];
291
292 for (order_side, side, levels, order_id_offset) in sides {
293 for (i, level) in levels.iter().enumerate() {
294 let Some((price_val, amount_val)) =
295 parse_snapshot_level(level, i, side, msg.instrument_name.as_str())
296 else {
297 continue;
298 };
299
300 if amount_val <= Decimal::ZERO {
301 continue;
302 }
303
304 let price = Price::from_decimal_dp(price_val, price_precision)?;
305 let size = Quantity::from_decimal_dp(amount_val, size_precision)?;
306
307 if is_zero_at_size_precision(size, amount_val, side, i, msg.instrument_name.as_str()) {
308 continue;
309 }
310
311 deltas.push(OrderBookDelta::new_checked(
312 instrument_id,
313 BookAction::Add,
314 BookOrder::new(order_side, price, size, (order_id_offset + i) as u64),
315 RecordFlag::F_SNAPSHOT as u8,
316 msg.change_id,
317 ts_event,
318 ts_init,
319 )?);
320 }
321 }
322
323 if let Some(last) = deltas.last_mut() {
324 *last = OrderBookDelta::new(
325 last.instrument_id,
326 last.action,
327 last.order,
328 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8,
329 last.sequence,
330 last.ts_event,
331 last.ts_init,
332 );
333 }
334
335 Ok(OrderBookDeltas::new(instrument_id, deltas))
336}
337
338pub fn parse_book_delta(
348 msg: &DeribitBookMsg<'_>,
349 instrument: &InstrumentAny,
350 ts_init: UnixNanos,
351) -> anyhow::Result<OrderBookDeltas> {
352 let instrument_id = instrument.id();
353 let price_precision = instrument.price_precision();
354 let size_precision = instrument.size_precision();
355 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
356
357 let mut deltas = Vec::new();
358
359 let sides = [
360 (OrderSide::Buy, "bid", &msg.bids, 0),
361 (OrderSide::Sell, "ask", &msg.asks, msg.bids.len()),
362 ];
363
364 for (order_side, side, levels, order_id_offset) in sides {
365 for (i, level) in levels.iter().enumerate() {
366 let Some((action, price_val, amount_val)) =
367 parse_delta_level(level, i, side, msg.instrument_name.as_str())
368 else {
369 continue;
370 };
371
372 let price = Price::from_decimal_dp(price_val, price_precision)?;
373 let size = Quantity::from_decimal_dp(amount_val.abs(), size_precision)?;
374
375 let action = if action != BookAction::Delete
376 && is_zero_at_size_precision(
377 size,
378 amount_val,
379 side,
380 i,
381 msg.instrument_name.as_str(),
382 ) {
383 BookAction::Delete
384 } else {
385 action
386 };
387
388 deltas.push(OrderBookDelta::new_checked(
389 instrument_id,
390 action,
391 BookOrder::new(order_side, price, size, (order_id_offset + i) as u64),
392 0,
393 msg.change_id,
394 ts_event,
395 ts_init,
396 )?);
397 }
398 }
399
400 if let Some(last) = deltas.last_mut() {
402 *last = OrderBookDelta::new(
403 last.instrument_id,
404 last.action,
405 last.order,
406 RecordFlag::F_LAST as u8,
407 last.sequence,
408 last.ts_event,
409 last.ts_init,
410 );
411 }
412
413 Ok(OrderBookDeltas::new(instrument_id, deltas))
414}
415
416pub fn parse_book_msg(
423 msg: &DeribitBookMsg<'_>,
424 instrument: &InstrumentAny,
425 ts_init: UnixNanos,
426) -> anyhow::Result<OrderBookDeltas> {
427 match msg.msg_type {
428 DeribitBookMsgType::Snapshot => parse_book_snapshot(msg, instrument, ts_init),
429 DeribitBookMsgType::Change => parse_book_delta(msg, instrument, ts_init),
430 }
431}
432
433pub(crate) fn parse_book_depth(
440 msg: &DeribitBookMsg<'_>,
441 instrument: &InstrumentAny,
442 ts_init: UnixNanos,
443) -> anyhow::Result<OrderBookDepth> {
444 let snapshot = parse_book_snapshot(msg, instrument, ts_init)?;
445 let (bids, asks): (Vec<_>, Vec<_>) = snapshot
446 .deltas
447 .iter()
448 .filter(|delta| delta.action == BookAction::Add)
449 .map(|delta| delta.order)
450 .partition(|order| order.side == OrderSide::Buy.into());
451 let bid_counts = vec![0; bids.len()];
452 let ask_counts = vec![0; asks.len()];
453
454 OrderBookDepth::new_checked(
455 snapshot.instrument_id,
456 bids,
457 asks,
458 bid_counts,
459 ask_counts,
460 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8,
461 msg.change_id,
462 snapshot.ts_event,
463 ts_init,
464 )
465}
466
467fn is_zero_at_size_precision(
470 size: Quantity,
471 amount: Decimal,
472 side: &str,
473 index: usize,
474 instrument_name: &str,
475) -> bool {
476 if size.is_positive() {
477 return false;
478 }
479
480 log::warn!(
481 "Book {side} amount {amount} at index {index} for {instrument_name} is zero at size precision {}, treating level as absent",
482 size.precision
483 );
484 true
485}
486
487pub fn parse_ticker_to_quote(
493 msg: &DeribitTickerMsg,
494 instrument: &InstrumentAny,
495 ts_init: UnixNanos,
496) -> anyhow::Result<QuoteTick> {
497 let instrument_id = instrument.id();
498 let price_precision = instrument.price_precision();
499 let size_precision = instrument.size_precision();
500
501 let bid_price_val = msg
502 .best_bid_price
503 .context("Missing best_bid_price in ticker")?;
504 let ask_price_val = msg
505 .best_ask_price
506 .context("Missing best_ask_price in ticker")?;
507
508 let bid_price = Price::from_decimal_dp(bid_price_val, price_precision)?;
509 let ask_price = Price::from_decimal_dp(ask_price_val, price_precision)?;
510 let bid_size =
511 Quantity::from_decimal_dp(msg.best_bid_amount.unwrap_or_default(), size_precision)?;
512 let ask_size =
513 Quantity::from_decimal_dp(msg.best_ask_amount.unwrap_or_default(), size_precision)?;
514 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
515
516 QuoteTick::new_checked(
517 instrument_id,
518 bid_price,
519 ask_price,
520 bid_size,
521 ask_size,
522 ts_event,
523 ts_init,
524 )
525}
526
527pub fn parse_quote_msg(
533 msg: &DeribitQuoteMsg,
534 instrument: &InstrumentAny,
535 ts_init: UnixNanos,
536) -> anyhow::Result<QuoteTick> {
537 let instrument_id = instrument.id();
538 let price_precision = instrument.price_precision();
539 let size_precision = instrument.size_precision();
540
541 let bid_price = Price::from_decimal_dp(msg.best_bid_price, price_precision)?;
542 let ask_price = Price::from_decimal_dp(msg.best_ask_price, price_precision)?;
543 let bid_size = Quantity::from_decimal_dp(msg.best_bid_amount, size_precision)?;
544 let ask_size = Quantity::from_decimal_dp(msg.best_ask_amount, size_precision)?;
545 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
546
547 QuoteTick::new_checked(
548 instrument_id,
549 bid_price,
550 ask_price,
551 bid_size,
552 ask_size,
553 ts_event,
554 ts_init,
555 )
556}
557
558pub fn parse_ticker_to_mark_price(
564 msg: &DeribitTickerMsg,
565 instrument: &InstrumentAny,
566 ts_init: UnixNanos,
567) -> anyhow::Result<MarkPriceUpdate> {
568 let instrument_id = instrument.id();
569 let price_precision = instrument.price_precision();
570 let value = Price::from_decimal_dp(msg.mark_price, price_precision)?;
571 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
572
573 Ok(MarkPriceUpdate::new(
574 instrument_id,
575 value,
576 ts_event,
577 ts_init,
578 ))
579}
580
581pub fn parse_ticker_to_index_price(
587 msg: &DeribitTickerMsg,
588 instrument: &InstrumentAny,
589 ts_init: UnixNanos,
590) -> anyhow::Result<IndexPriceUpdate> {
591 let instrument_id = instrument.id();
592 let price_precision = instrument.price_precision();
593 let value = Price::from_decimal_dp(msg.index_price, price_precision)?;
594 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
595
596 Ok(IndexPriceUpdate::new(
597 instrument_id,
598 value,
599 ts_event,
600 ts_init,
601 ))
602}
603
604#[must_use]
608pub fn parse_ticker_to_funding_rate(
609 msg: &DeribitTickerMsg,
610 instrument: &InstrumentAny,
611 ts_init: UnixNanos,
612) -> Option<FundingRateUpdate> {
613 let rate = msg.current_funding?;
615 let instrument_id = instrument.id();
616 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
617
618 Some(FundingRateUpdate::new(
620 instrument_id,
621 rate,
622 None, None, ts_event,
625 ts_init,
626 ))
627}
628
629#[must_use]
633pub fn parse_ticker_to_option_greeks(
634 msg: &DeribitTickerMsg,
635 instrument: &InstrumentAny,
636 ts_init: UnixNanos,
637) -> Option<OptionGreeks> {
638 let deribit_greeks = msg.greeks.as_ref()?;
639 let instrument_id = instrument.id();
640 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
641 let iv_to_fraction = |iv: Decimal| iv.to_f64().map(|v| v / 100.0);
642
643 Some(OptionGreeks {
644 instrument_id,
645 convention: GreeksConvention::BlackScholes,
646 greeks: deribit_greeks.to_greek_values(),
647 mark_iv: msg.mark_iv.and_then(iv_to_fraction),
648 bid_iv: msg.bid_iv.and_then(iv_to_fraction),
649 ask_iv: msg.ask_iv.and_then(iv_to_fraction),
650 underlying_price: msg.underlying_price.and_then(|v| v.to_f64()),
651 open_interest: Some(msg.open_interest.to_f64().unwrap_or(0.0)),
652 ts_event,
653 ts_init,
654 })
655}
656
657#[must_use]
662pub fn parse_perpetual_to_funding_rate(
663 msg: &DeribitPerpetualMsg,
664 instrument: &InstrumentAny,
665 ts_init: UnixNanos,
666) -> FundingRateUpdate {
667 let instrument_id = instrument.id();
668 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
669
670 FundingRateUpdate::new(
671 instrument_id,
672 msg.interest,
673 None, None, ts_event,
676 ts_init,
677 )
678}
679
680pub fn resolution_to_bar_type(
688 instrument_id: InstrumentId,
689 resolution: &str,
690) -> anyhow::Result<BarType> {
691 let (step, aggregation) = match resolution {
692 "1" => (1, BarAggregation::Minute),
693 "3" => (3, BarAggregation::Minute),
694 "5" => (5, BarAggregation::Minute),
695 "10" => (10, BarAggregation::Minute),
696 "15" => (15, BarAggregation::Minute),
697 "30" => (30, BarAggregation::Minute),
698 "60" => (1, BarAggregation::Hour),
699 "120" => (2, BarAggregation::Hour),
700 "180" => (3, BarAggregation::Hour),
701 "360" => (6, BarAggregation::Hour),
702 "720" => (12, BarAggregation::Hour),
703 "1D" => (1, BarAggregation::Day),
704 _ => anyhow::bail!("Unsupported Deribit resolution: {resolution}"),
705 };
706
707 let spec = BarSpecification::new_checked(step, aggregation, PriceType::Last)
708 .context("invalid Deribit bar resolution")?;
709 Ok(BarType::new(
710 instrument_id,
711 spec,
712 AggregationSource::External,
713 ))
714}
715
716pub fn parse_chart_msg(
732 chart_msg: &DeribitChartMsg,
733 bar_type: BarType,
734 price_precision: u8,
735 size_precision: u8,
736 use_cost_for_volume: bool,
737 timestamp_on_close: bool,
738 ts_init: UnixNanos,
739) -> anyhow::Result<Bar> {
740 let open =
741 Price::from_decimal_dp(chart_msg.open, price_precision).context("Invalid open price")?;
742 let high =
743 Price::from_decimal_dp(chart_msg.high, price_precision).context("Invalid high price")?;
744 let low =
745 Price::from_decimal_dp(chart_msg.low, price_precision).context("Invalid low price")?;
746 let close =
747 Price::from_decimal_dp(chart_msg.close, price_precision).context("Invalid close price")?;
748
749 let raw_volume = if use_cost_for_volume {
750 chart_msg.cost
751 } else {
752 chart_msg.volume
753 };
754
755 let volume = Quantity::from_decimal_dp(raw_volume, size_precision).context("Invalid volume")?;
756
757 let mut ts_event = UnixNanos::from(chart_msg.tick * NANOSECONDS_IN_MILLISECOND);
759
760 if timestamp_on_close {
762 let interval_ns = bar_type.spec().timedelta().as_nanos();
763 let interval_ns = u64::try_from(interval_ns)
764 .context("bar interval overflowed the u64 range for nanoseconds")?;
765 let updated = ts_event
766 .as_u64()
767 .checked_add(interval_ns)
768 .context("bar timestamp overflowed when adjusting to close time")?;
769 ts_event = UnixNanos::from(updated);
770 }
771
772 Bar::new_checked(bar_type, open, high, low, close, volume, ts_event, ts_init)
773 .context("Invalid OHLC bar")
774}
775
776pub fn parse_user_order_msg(
782 msg: &DeribitOrderMsg,
783 instrument: &InstrumentAny,
784 account_id: AccountId,
785 ts_init: UnixNanos,
786) -> anyhow::Result<OrderStatusReport> {
787 let instrument_id = instrument.id();
788 let venue_order_id = VenueOrderId::new(&msg.order_id);
789
790 let order_side = match msg.direction.as_str() {
791 "buy" => OrderSide::Buy,
792 "sell" => OrderSide::Sell,
793 _ => anyhow::bail!("Unknown order direction: {}", msg.direction),
794 };
795
796 let order_type = parse_deribit_order_type(&msg.order_type);
798
799 let time_in_force = match msg.time_in_force.as_str() {
801 "good_til_cancelled" => TimeInForce::Gtc,
802 "good_til_day" => TimeInForce::Gtd,
803 "fill_or_kill" => TimeInForce::Fok,
804 "immediate_or_cancel" => TimeInForce::Ioc,
805 other => {
806 log::warn!("Unknown time_in_force '{other}', defaulting to GTC");
807 TimeInForce::Gtc
808 }
809 };
810
811 let order_status = match msg.order_state.as_str() {
813 "open" => {
814 if msg.filled_amount.is_zero() {
815 OrderStatus::Accepted
816 } else {
817 OrderStatus::PartiallyFilled
818 }
819 }
820 "filled" => OrderStatus::Filled,
821 "rejected" => OrderStatus::Rejected,
822 "cancelled" => OrderStatus::Canceled,
823 "untriggered" => OrderStatus::Accepted, other => {
825 log::warn!("Unknown Deribit order_state '{other}', defaulting to Accepted");
826 OrderStatus::Accepted
827 }
828 };
829
830 let price_precision = instrument.price_precision();
831 let size_precision = instrument.size_precision();
832
833 let quantity = Quantity::from_decimal_dp(msg.amount, size_precision)?;
834 let filled_qty = Quantity::from_decimal_dp(msg.filled_amount, size_precision)?;
835
836 let ts_accepted = UnixNanos::new(msg.creation_timestamp * NANOSECONDS_IN_MILLISECOND);
837 let ts_last = UnixNanos::new(msg.last_update_timestamp * NANOSECONDS_IN_MILLISECOND);
838
839 let mut report = OrderStatusReport::new(
840 account_id,
841 instrument_id,
842 None, venue_order_id,
844 order_side.into(),
845 order_type,
846 time_in_force,
847 order_status,
848 quantity,
849 filled_qty,
850 ts_accepted,
851 ts_last,
852 ts_init,
853 Some(UUID4::new()),
854 );
855
856 if let Some(ref label) = msg.label
858 && !label.is_empty()
859 {
860 report = report.with_client_order_id(ClientOrderId::new(label));
861 }
862
863 if let Some(price_val) = msg.price
865 && !price_val.is_zero()
866 {
867 let price = Price::from_decimal_dp(price_val, price_precision)?;
868 report = report.with_price(price);
869 }
870
871 if time_in_force == TimeInForce::Gtd {
872 let expire_time = next_8_utc(ts_accepted)?;
873 report = report.with_expire_time(expire_time);
874 }
875
876 if let Some(avg_price) = msg.average_price
878 && !avg_price.is_zero()
879 {
880 report = report.with_avg_px(avg_price);
881 }
882
883 if let Some(trigger_price) = msg.trigger_price
885 && !trigger_price.is_zero()
886 {
887 let trigger = Price::from_decimal_dp(trigger_price, price_precision)?;
888 report = report.with_trigger_price(trigger);
889 }
890
891 if msg.post_only {
892 report = report.with_post_only(true);
893 }
894
895 if msg.reduce_only {
896 report = report.with_reduce_only(true);
897 }
898
899 if let Some(ref reason) = msg.reject_reason {
901 report = report.with_cancel_reason(reason.clone());
902 } else if let Some(ref reason) = msg.cancel_reason {
903 report = report.with_cancel_reason(reason.clone());
904 }
905
906 Ok(report)
907}
908
909#[must_use]
910pub(crate) fn parse_deribit_order_type(order_type: &str) -> OrderType {
911 match order_type {
912 "limit" => OrderType::Limit,
913 "market" => OrderType::Market,
914 "stop_limit" => OrderType::StopLimit,
915 "stop_market" => OrderType::StopMarket,
916 "take_limit" => OrderType::LimitIfTouched,
917 "take_market" => OrderType::MarketIfTouched,
918 other => {
919 log::warn!("Unknown Deribit order_type '{other}', defaulting to Limit");
920 OrderType::Limit
921 }
922 }
923}
924
925pub fn parse_user_trade_msg(
931 msg: &DeribitUserTradeMsg,
932 instrument: &InstrumentAny,
933 account_id: AccountId,
934 ts_init: UnixNanos,
935) -> anyhow::Result<FillReport> {
936 let instrument_id = instrument.id();
937 let venue_order_id = VenueOrderId::new(&msg.order_id);
938 let trade_id = TradeId::new(&msg.trade_id);
939
940 if let Some(liq) = msg.liquidation.as_deref().filter(|s| !s.is_empty()) {
943 let who = match liq {
944 "M" => "maker",
945 "T" => "taker",
946 "MT" => "both",
947 _ => liq,
948 };
949 log::warn!(
950 "Liquidation trade: {} trade_id={} order_id={} liquidation_side={} direction={} amount={} price={}",
951 instrument_id,
952 msg.trade_id,
953 msg.order_id,
954 who,
955 msg.direction,
956 msg.amount,
957 msg.price,
958 );
959 }
960
961 let order_side = match msg.direction.as_str() {
962 "buy" => OrderSide::Buy,
963 "sell" => OrderSide::Sell,
964 _ => anyhow::bail!("Unknown trade direction: {}", msg.direction),
965 };
966
967 let price_precision = instrument.price_precision();
968 let size_precision = instrument.size_precision();
969
970 let last_qty = Quantity::from_decimal_dp(msg.amount, size_precision)?;
971 let last_px = Price::from_decimal_dp(msg.price, price_precision)?;
972
973 let liquidity_side = match msg.liquidity.as_str() {
974 "M" => LiquiditySide::Maker,
975 "T" => LiquiditySide::Taker,
976 _ => LiquiditySide::NoLiquiditySide,
977 };
978
979 let fee_currency = Currency::from(&msg.fee_currency);
981 let commission = Money::from_decimal(msg.fee, fee_currency)?;
982
983 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
984
985 let client_order_id = msg
986 .label
987 .as_ref()
988 .filter(|l| !l.is_empty())
989 .map(ClientOrderId::new);
990
991 Ok(FillReport::new(
992 account_id,
993 instrument_id,
994 venue_order_id,
995 trade_id,
996 order_side,
997 last_qty,
998 last_px,
999 commission,
1000 liquidity_side,
1001 client_order_id,
1002 None, ts_event,
1004 ts_init,
1005 None, ))
1007}
1008
1009#[must_use]
1020pub fn parse_position_status_report(
1021 position: &DeribitPosition,
1022 instrument: &InstrumentAny,
1023 account_id: AccountId,
1024 ts_init: UnixNanos,
1025) -> PositionStatusReport {
1026 let instrument_id = instrument.id();
1027 let size_precision = instrument.size_precision();
1028
1029 let signed_qty = Quantity::from_decimal_dp(position.size.abs(), size_precision)
1030 .unwrap_or_else(|_| Quantity::zero(size_precision));
1031
1032 let position_side = match position.direction.as_str() {
1033 "buy" => PositionSide::Long,
1034 "sell" => PositionSide::Short,
1035 _ => PositionSide::Flat,
1036 };
1037
1038 let avg_px_open = Some(position.average_price);
1040
1041 PositionStatusReport::new(
1042 account_id,
1043 instrument_id,
1044 position_side,
1045 signed_qty,
1046 ts_init,
1047 ts_init,
1048 Some(UUID4::new()),
1049 None, avg_px_open,
1051 )
1052}
1053
1054#[derive(Debug, Clone)]
1059pub enum ParsedOrderEvent {
1060 Accepted(OrderAccepted),
1062 Canceled(OrderCanceled),
1064 Expired(OrderExpired),
1066 Updated(OrderUpdated),
1068 None,
1070}
1071
1072fn extract_client_order_id(msg: &DeribitOrderMsg) -> Option<ClientOrderId> {
1074 msg.label
1075 .as_ref()
1076 .filter(|l| !l.is_empty())
1077 .map(ClientOrderId::new)
1078}
1079
1080#[must_use]
1085pub fn parse_order_accepted(
1086 msg: &DeribitOrderMsg,
1087 instrument: &InstrumentAny,
1088 account_id: AccountId,
1089 trader_id: TraderId,
1090 strategy_id: StrategyId,
1091 ts_init: UnixNanos,
1092) -> OrderAccepted {
1093 let client_order_id =
1094 extract_client_order_id(msg).unwrap_or_else(|| ClientOrderId::new(&msg.order_id));
1095 parse_order_accepted_with_client_order_id(
1096 msg,
1097 instrument,
1098 account_id,
1099 trader_id,
1100 strategy_id,
1101 client_order_id,
1102 ts_init,
1103 )
1104}
1105
1106#[must_use]
1107pub(crate) fn parse_order_accepted_with_client_order_id(
1108 msg: &DeribitOrderMsg,
1109 instrument: &InstrumentAny,
1110 account_id: AccountId,
1111 trader_id: TraderId,
1112 strategy_id: StrategyId,
1113 client_order_id: ClientOrderId,
1114 ts_init: UnixNanos,
1115) -> OrderAccepted {
1116 let instrument_id = instrument.id();
1117 let venue_order_id = VenueOrderId::new(&msg.order_id);
1118 let ts_event = UnixNanos::new(msg.last_update_timestamp * NANOSECONDS_IN_MILLISECOND);
1119
1120 OrderAccepted::new(
1121 trader_id,
1122 strategy_id,
1123 instrument_id,
1124 client_order_id,
1125 venue_order_id,
1126 account_id,
1127 nautilus_core::UUID4::new(),
1128 ts_event,
1129 ts_init,
1130 false, )
1132}
1133
1134#[must_use]
1138pub fn parse_order_canceled(
1139 msg: &DeribitOrderMsg,
1140 instrument: &InstrumentAny,
1141 account_id: AccountId,
1142 trader_id: TraderId,
1143 strategy_id: StrategyId,
1144 ts_init: UnixNanos,
1145) -> OrderCanceled {
1146 let client_order_id =
1147 extract_client_order_id(msg).unwrap_or_else(|| ClientOrderId::new(&msg.order_id));
1148 parse_order_canceled_with_client_order_id(
1149 msg,
1150 instrument,
1151 account_id,
1152 trader_id,
1153 strategy_id,
1154 client_order_id,
1155 ts_init,
1156 )
1157}
1158
1159#[must_use]
1160pub(crate) fn parse_order_canceled_with_client_order_id(
1161 msg: &DeribitOrderMsg,
1162 instrument: &InstrumentAny,
1163 account_id: AccountId,
1164 trader_id: TraderId,
1165 strategy_id: StrategyId,
1166 client_order_id: ClientOrderId,
1167 ts_init: UnixNanos,
1168) -> OrderCanceled {
1169 let instrument_id = instrument.id();
1170 let venue_order_id = VenueOrderId::new(&msg.order_id);
1171 let ts_event = UnixNanos::new(msg.last_update_timestamp * NANOSECONDS_IN_MILLISECOND);
1172
1173 OrderCanceled::new(
1174 trader_id,
1175 strategy_id,
1176 instrument_id,
1177 client_order_id,
1178 nautilus_core::UUID4::new(),
1179 ts_event,
1180 ts_init,
1181 false, Some(venue_order_id),
1183 Some(account_id),
1184 msg.cancel_reason.as_deref().map(Ustr::from),
1185 )
1186}
1187
1188#[must_use]
1193pub fn parse_order_expired(
1194 msg: &DeribitOrderMsg,
1195 instrument: &InstrumentAny,
1196 account_id: AccountId,
1197 trader_id: TraderId,
1198 strategy_id: StrategyId,
1199 ts_init: UnixNanos,
1200) -> OrderExpired {
1201 let client_order_id =
1202 extract_client_order_id(msg).unwrap_or_else(|| ClientOrderId::new(&msg.order_id));
1203 parse_order_expired_with_client_order_id(
1204 msg,
1205 instrument,
1206 account_id,
1207 trader_id,
1208 strategy_id,
1209 client_order_id,
1210 ts_init,
1211 )
1212}
1213
1214#[must_use]
1215pub(crate) fn parse_order_expired_with_client_order_id(
1216 msg: &DeribitOrderMsg,
1217 instrument: &InstrumentAny,
1218 account_id: AccountId,
1219 trader_id: TraderId,
1220 strategy_id: StrategyId,
1221 client_order_id: ClientOrderId,
1222 ts_init: UnixNanos,
1223) -> OrderExpired {
1224 let instrument_id = instrument.id();
1225 let venue_order_id = VenueOrderId::new(&msg.order_id);
1226 let ts_event = UnixNanos::new(msg.last_update_timestamp * NANOSECONDS_IN_MILLISECOND);
1227
1228 OrderExpired::new(
1229 trader_id,
1230 strategy_id,
1231 instrument_id,
1232 client_order_id,
1233 nautilus_core::UUID4::new(),
1234 ts_event,
1235 ts_init,
1236 false, Some(venue_order_id),
1238 Some(account_id),
1239 )
1240}
1241
1242#[must_use]
1246pub fn parse_order_updated(
1247 msg: &DeribitOrderMsg,
1248 instrument: &InstrumentAny,
1249 account_id: AccountId,
1250 trader_id: TraderId,
1251 strategy_id: StrategyId,
1252 ts_init: UnixNanos,
1253) -> OrderUpdated {
1254 let client_order_id =
1255 extract_client_order_id(msg).unwrap_or_else(|| ClientOrderId::new(&msg.order_id));
1256 parse_order_updated_with_client_order_id(
1257 msg,
1258 instrument,
1259 account_id,
1260 trader_id,
1261 strategy_id,
1262 client_order_id,
1263 ts_init,
1264 )
1265}
1266
1267#[must_use]
1268pub(crate) fn parse_order_updated_with_client_order_id(
1269 msg: &DeribitOrderMsg,
1270 instrument: &InstrumentAny,
1271 account_id: AccountId,
1272 trader_id: TraderId,
1273 strategy_id: StrategyId,
1274 client_order_id: ClientOrderId,
1275 ts_init: UnixNanos,
1276) -> OrderUpdated {
1277 let instrument_id = instrument.id();
1278 let price_precision = instrument.price_precision();
1279 let size_precision = instrument.size_precision();
1280
1281 let venue_order_id = VenueOrderId::new(&msg.order_id);
1282 let quantity = Quantity::from_decimal_dp(msg.amount, size_precision)
1283 .unwrap_or_else(|_| Quantity::zero(size_precision));
1284 let price = msg
1285 .price
1286 .and_then(|p| Price::from_decimal_dp(p, price_precision).ok());
1287 let trigger_price = msg
1288 .trigger_price
1289 .and_then(|p| Price::from_decimal_dp(p, price_precision).ok());
1290 let ts_event = UnixNanos::new(msg.last_update_timestamp * NANOSECONDS_IN_MILLISECOND);
1291
1292 OrderUpdated::new(
1293 trader_id,
1294 strategy_id,
1295 instrument_id,
1296 client_order_id,
1297 quantity,
1298 nautilus_core::UUID4::new(),
1299 ts_event,
1300 ts_init,
1301 false, Some(venue_order_id),
1303 Some(account_id),
1304 price,
1305 trigger_price,
1306 None, false, )
1309}
1310
1311#[must_use]
1324pub fn determine_order_event_type(
1325 order_state: &str,
1326 is_new_order: bool,
1327 was_amended: bool,
1328) -> OrderEventType {
1329 match order_state {
1330 "open" | "untriggered" => {
1331 if was_amended {
1332 OrderEventType::Updated
1333 } else if is_new_order {
1334 OrderEventType::Accepted
1335 } else {
1336 OrderEventType::None
1338 }
1339 }
1340 "cancelled" => OrderEventType::Canceled,
1341 "expired" => OrderEventType::Expired,
1342 "filled" => {
1343 OrderEventType::None
1345 }
1346 "rejected" => {
1347 OrderEventType::None
1349 }
1350 other => {
1351 log::warn!("Unknown Deribit order_state '{other}' in event routing, dropping");
1352 OrderEventType::None
1353 }
1354 }
1355}
1356
1357#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1359pub enum OrderEventType {
1360 Accepted,
1362 Canceled,
1364 Expired,
1366 Updated,
1368 None,
1370}
1371
1372#[cfg(test)]
1373mod tests {
1374 use nautilus_model::data::greeks::OptionGreekValues;
1375 use rstest::{fixture, rstest};
1376 use rust_decimal_macros::dec;
1377 use serde::de::DeserializeOwned;
1378
1379 use super::*;
1380 use crate::{
1381 common::{
1382 parse::{parse_deribit_instrument_any, parse_portfolio_to_account_state},
1383 testing::load_test_json,
1384 },
1385 http::models::{DeribitInstrument, DeribitJsonRpcResponse},
1386 websocket::messages::{DeribitPortfolioMsg, DeribitWsMessage, parse_raw_message},
1387 };
1388
1389 fn test_perpetual_instrument() -> InstrumentAny {
1391 let json = load_test_json("http_get_instruments.json");
1392 let response: DeribitJsonRpcResponse<Vec<DeribitInstrument>> =
1393 serde_json::from_str(&json).unwrap();
1394 let instrument = &response.result.unwrap()[0];
1395 parse_deribit_instrument_any(instrument, UnixNanos::default(), UnixNanos::default())
1396 .unwrap()
1397 .unwrap()
1398 }
1399
1400 #[rstest]
1401 fn test_parse_trade_msg_sell() {
1402 let instrument = test_perpetual_instrument();
1403 let json = load_test_json("ws_trades.json");
1404 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1405 let trades: Vec<DeribitTradeMsg> =
1406 serde_json::from_str(&response["params"]["data"].to_string()).unwrap();
1407 let msg = &trades[0];
1408
1409 let tick = parse_trade_msg(msg, &instrument, UnixNanos::default()).unwrap();
1410
1411 assert_eq!(tick.instrument_id, instrument.id());
1412 assert_eq!(tick.price, instrument.make_price(92294.5));
1413 assert_eq!(tick.size, instrument.make_qty(10.0, None));
1414 assert_eq!(tick.aggressor_side, AggressorSide::Sell);
1415 assert_eq!(tick.trade_id.to_string(), "403691824");
1416 assert_eq!(tick.ts_event, UnixNanos::new(1_765_531_356_452_000_000));
1417 }
1418
1419 #[rstest]
1420 fn test_parse_trade_msg_buy() {
1421 let instrument = test_perpetual_instrument();
1422 let json = load_test_json("ws_trades.json");
1423 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1424 let trades: Vec<DeribitTradeMsg> =
1425 serde_json::from_str(&response["params"]["data"].to_string()).unwrap();
1426 let msg = &trades[1];
1427
1428 let tick = parse_trade_msg(msg, &instrument, UnixNanos::default()).unwrap();
1429
1430 assert_eq!(tick.instrument_id, instrument.id());
1431 assert_eq!(tick.price, instrument.make_price(92288.5));
1432 assert_eq!(tick.size, instrument.make_qty(750.0, None));
1433 assert_eq!(tick.aggressor_side, AggressorSide::Sell);
1434 assert_eq!(tick.trade_id.to_string(), "403691825");
1435 }
1436
1437 fn make_trade_msg(
1438 instrument_name: &str,
1439 trade_id: &str,
1440 block_trade_id: Option<&str>,
1441 block_rfq_id: Option<i64>,
1442 combo_id: Option<&str>,
1443 ) -> DeribitTradeMsg {
1444 let raw = serde_json::json!({
1445 "trade_id": trade_id,
1446 "instrument_name": instrument_name,
1447 "price": 92294.5,
1448 "amount": 10.0,
1449 "direction": "buy",
1450 "timestamp": 1_765_531_356_452_u64,
1451 "trade_seq": 1,
1452 "tick_direction": 0,
1453 "index_price": 92276.75,
1454 "mark_price": 92287.11,
1455 "block_trade_id": block_trade_id,
1456 "block_rfq_id": block_rfq_id,
1457 "combo_id": combo_id,
1458 });
1459 serde_json::from_str(&raw.to_string()).unwrap()
1460 }
1461
1462 #[rstest]
1463 fn test_parse_trade_msg_tags_block_trade() {
1464 let instrument = test_perpetual_instrument();
1465 let msg = make_trade_msg("BTC-PERPETUAL", "244343055", Some("12345"), None, None);
1466 let tick = parse_trade_msg(&msg, &instrument, UnixNanos::default()).unwrap();
1467 assert_eq!(tick.trade_id.to_string(), "BLK-244343055");
1468 }
1469
1470 #[rstest]
1471 fn test_parse_trade_msg_tags_block_rfq() {
1472 let instrument = test_perpetual_instrument();
1473 let msg = make_trade_msg("BTC-PERPETUAL", "244343055", None, Some(99), None);
1474 let tick = parse_trade_msg(&msg, &instrument, UnixNanos::default()).unwrap();
1475 assert_eq!(tick.trade_id.to_string(), "RFQ-244343055");
1476 }
1477
1478 #[rstest]
1479 fn test_parse_trade_msg_tags_combo_leg() {
1480 let instrument = test_perpetual_instrument();
1484 let msg = make_trade_msg(
1485 "BTC-PERPETUAL",
1486 "244343055",
1487 None,
1488 None,
1489 Some("BTC-FS-25DEC26_PERP"),
1490 );
1491 let tick = parse_trade_msg(&msg, &instrument, UnixNanos::default()).unwrap();
1492 assert_eq!(tick.trade_id.to_string(), "COMBO-244343055");
1493 }
1494
1495 fn load_combo_option_instrument() -> InstrumentAny {
1496 let combo_json = load_test_json("http_get_instruments_option_combo.json");
1497 let combo_response: DeribitJsonRpcResponse<Vec<DeribitInstrument>> =
1498 serde_json::from_str(&combo_json).unwrap();
1499 let combo_raw = combo_response
1500 .result
1501 .unwrap()
1502 .into_iter()
1503 .find(|i| i.instrument_name == "BTC-CS-19MAY26-70000_75000")
1504 .expect("fixture must contain BTC-CS-19MAY26-70000_75000");
1505 parse_deribit_instrument_any(&combo_raw, UnixNanos::default(), UnixNanos::default())
1506 .unwrap()
1507 .unwrap()
1508 }
1509
1510 fn load_combo_trade_msgs() -> Vec<DeribitTradeMsg> {
1511 let json = load_test_json("ws_trades_option_combo.json");
1512 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1513 serde_json::from_str(&response["params"]["data"].to_string()).unwrap()
1514 }
1515
1516 #[rstest]
1517 fn test_parse_trades_data_combo_emits_single_tick() {
1518 let combo_inst = load_combo_option_instrument();
1522 let mut cache: AHashMap<Ustr, InstrumentAny> = AHashMap::new();
1523 cache.insert(Ustr::from("BTC-CS-19MAY26-70000_75000"), combo_inst.clone());
1524
1525 let trades = load_combo_trade_msgs();
1526 let legs = trades[0].legs.as_ref().expect("combo trade must have legs");
1528 assert_eq!(legs.len(), 2);
1529
1530 let data = parse_trades_data(&trades, &cache, UnixNanos::default());
1531 assert_eq!(data.len(), 1, "should emit one tick for the combo only");
1532
1533 let Data::Trade(tick) = &data[0] else {
1534 panic!("expected Data::Trade");
1535 };
1536 assert_eq!(tick.instrument_id, combo_inst.id());
1537 assert_eq!(tick.price, Price::from("0.0639"));
1540 assert_eq!(tick.size, Quantity::from("0.1"));
1541 assert_eq!(tick.aggressor_side, AggressorSide::Sell);
1542 assert_eq!(tick.trade_id.to_string(), "244365193");
1543 }
1544
1545 #[rstest]
1546 fn test_parse_trades_data_combo_not_cached_emits_no_tick() {
1547 let cache: AHashMap<Ustr, InstrumentAny> = AHashMap::new();
1551 let trades = load_combo_trade_msgs();
1552 let data = parse_trades_data(&trades, &cache, UnixNanos::default());
1553 assert!(data.is_empty(), "uncached combo must not emit ticks");
1554 }
1555
1556 #[rstest]
1557 fn test_parse_trades_data_mixed_combo_and_per_leg() {
1558 let combo_inst = load_combo_option_instrument();
1561 let perp_inst = test_perpetual_instrument();
1562
1563 let mut cache: AHashMap<Ustr, InstrumentAny> = AHashMap::new();
1564 cache.insert(Ustr::from("BTC-CS-19MAY26-70000_75000"), combo_inst.clone());
1565 cache.insert(Ustr::from("BTC-PERPETUAL"), perp_inst.clone());
1566
1567 let mut trades = load_combo_trade_msgs();
1568 let perp_msgs: Vec<DeribitTradeMsg> = {
1569 let json = load_test_json("ws_trades.json");
1570 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1571 serde_json::from_str(&response["params"]["data"].to_string()).unwrap()
1572 };
1573 trades.extend(perp_msgs);
1574
1575 let data = parse_trades_data(&trades, &cache, UnixNanos::default());
1576 assert_eq!(data.len(), 3);
1578
1579 let mut combo_ticks = 0;
1580 let mut perp_ticks = 0;
1581
1582 for item in &data {
1583 let Data::Trade(tick) = item else {
1584 panic!("expected Data::Trade, was {item:?}");
1585 };
1586
1587 if tick.instrument_id == combo_inst.id() {
1588 combo_ticks += 1;
1589 assert_eq!(tick.trade_id.to_string(), "244365193");
1590 } else if tick.instrument_id == perp_inst.id() {
1591 perp_ticks += 1;
1592 } else {
1593 panic!("unexpected instrument_id: {}", tick.instrument_id);
1594 }
1595 }
1596 assert_eq!(combo_ticks, 1);
1597 assert_eq!(perp_ticks, 2);
1598 }
1599
1600 #[rstest]
1601 fn test_parse_book_snapshot() {
1602 let instrument = test_perpetual_instrument();
1603 let json = load_test_json("ws_book_snapshot.json");
1604 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1605 let data = response["params"]["data"].to_string();
1606 let msg: DeribitBookMsg = serde_json::from_str(&data).unwrap();
1607
1608 let deltas = parse_book_snapshot(&msg, &instrument, UnixNanos::default()).unwrap();
1609
1610 assert_eq!(deltas.instrument_id, instrument.id());
1611 assert_eq!(deltas.deltas.len(), 11);
1613
1614 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
1616
1617 let first_bid = &deltas.deltas[1];
1619 assert_eq!(first_bid.action, BookAction::Add);
1620 assert_eq!(first_bid.order.side, OrderSide::Buy.into());
1621 assert_eq!(first_bid.order.price, instrument.make_price(42500.0));
1622 assert_eq!(first_bid.order.size, instrument.make_qty(1000.0, None));
1623
1624 let first_ask = &deltas.deltas[6];
1626 assert_eq!(first_ask.action, BookAction::Add);
1627 assert_eq!(first_ask.order.side, OrderSide::Sell.into());
1628 assert_eq!(first_ask.order.price, instrument.make_price(42501.0));
1629 assert_eq!(first_ask.order.size, instrument.make_qty(800.0, None));
1630
1631 let last = deltas.deltas.last().unwrap();
1633 assert_eq!(
1634 last.flags & RecordFlag::F_LAST as u8,
1635 RecordFlag::F_LAST as u8
1636 );
1637 }
1638
1639 #[rstest]
1640 fn test_parse_book_delta() {
1641 let instrument = test_perpetual_instrument();
1642 let json = load_test_json("ws_book_delta.json");
1643 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1644 let data = response["params"]["data"].to_string();
1645 let msg: DeribitBookMsg = serde_json::from_str(&data).unwrap();
1646
1647 let deltas = parse_book_delta(&msg, &instrument, UnixNanos::default()).unwrap();
1648
1649 assert_eq!(deltas.instrument_id, instrument.id());
1650 assert_eq!(deltas.deltas.len(), 4);
1652
1653 let bid_change = &deltas.deltas[0];
1655 assert_eq!(bid_change.action, BookAction::Update);
1656 assert_eq!(bid_change.order.side, OrderSide::Buy.into());
1657 assert_eq!(bid_change.order.price, instrument.make_price(42500.0));
1658 assert_eq!(bid_change.order.size, instrument.make_qty(950.0, None));
1659
1660 let bid_new = &deltas.deltas[1];
1662 assert_eq!(bid_new.action, BookAction::Add);
1663 assert_eq!(bid_new.order.side, OrderSide::Buy.into());
1664 assert_eq!(bid_new.order.price, instrument.make_price(42498.5));
1665 assert_eq!(bid_new.order.size, instrument.make_qty(300.0, None));
1666
1667 let ask_delete = &deltas.deltas[2];
1669 assert_eq!(ask_delete.action, BookAction::Delete);
1670 assert_eq!(ask_delete.order.side, OrderSide::Sell.into());
1671 assert_eq!(ask_delete.order.price, instrument.make_price(42501.0));
1672 assert_eq!(ask_delete.order.size, instrument.make_qty(0.0, None));
1673
1674 let ask_change = &deltas.deltas[3];
1676 assert_eq!(ask_change.action, BookAction::Update);
1677 assert_eq!(ask_change.order.side, OrderSide::Sell.into());
1678 assert_eq!(ask_change.order.price, instrument.make_price(42501.5));
1679 assert_eq!(ask_change.order.size, instrument.make_qty(700.0, None));
1680
1681 let last = deltas.deltas.last().unwrap();
1683 assert_eq!(
1684 last.flags & RecordFlag::F_LAST as u8,
1685 RecordFlag::F_LAST as u8
1686 );
1687 }
1688
1689 #[rstest]
1690 fn test_parse_ticker_to_quote() {
1691 let instrument = test_perpetual_instrument();
1692 let json = load_test_json("ws_ticker.json");
1693 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1694 let msg: DeribitTickerMsg =
1695 serde_json::from_str(&response["params"]["data"].to_string()).unwrap();
1696
1697 assert_eq!(msg.instrument_name, "BTC-PERPETUAL");
1699 assert_eq!(msg.timestamp, 1_765_541_474_086);
1700 assert_eq!(msg.best_bid_price, Some(dec!(92283.5)));
1701 assert_eq!(msg.best_ask_price, Some(dec!(92284.0)));
1702 assert_eq!(msg.best_bid_amount, Some(dec!(117660.0)));
1703 assert_eq!(msg.best_ask_amount, Some(dec!(186520.0)));
1704 assert_eq!(msg.mark_price, dec!(92281.78));
1705 assert_eq!(msg.index_price, dec!(92263.55));
1706 assert_eq!(msg.open_interest, dec!(1132329370.0));
1707
1708 let quote = parse_ticker_to_quote(&msg, &instrument, UnixNanos::default()).unwrap();
1709
1710 assert_eq!(quote.instrument_id, instrument.id());
1711 assert_eq!(quote.bid_price, instrument.make_price(92283.5));
1712 assert_eq!(quote.ask_price, instrument.make_price(92284.0));
1713 assert_eq!(quote.bid_size, instrument.make_qty(117660.0, None));
1714 assert_eq!(quote.ask_size, instrument.make_qty(186520.0, None));
1715 assert_eq!(quote.ts_event, UnixNanos::new(1_765_541_474_086_000_000));
1716 }
1717
1718 #[rstest]
1719 fn test_parse_quote_msg() {
1720 let instrument = test_perpetual_instrument();
1721 let json = load_test_json("ws_quote.json");
1722 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1723 let msg: DeribitQuoteMsg =
1724 serde_json::from_str(&response["params"]["data"].to_string()).unwrap();
1725
1726 assert_eq!(msg.instrument_name, "BTC-PERPETUAL");
1728 assert_eq!(msg.timestamp, 1_765_541_767_174);
1729 assert_eq!(msg.best_bid_price, dec!(92288.0));
1730 assert_eq!(msg.best_ask_price, dec!(92288.5));
1731 assert_eq!(msg.best_bid_amount, dec!(133440.0));
1732 assert_eq!(msg.best_ask_amount, dec!(99470.0));
1733
1734 let quote = parse_quote_msg(&msg, &instrument, UnixNanos::default()).unwrap();
1735
1736 assert_eq!(quote.instrument_id, instrument.id());
1737 assert_eq!(quote.bid_price, instrument.make_price(92288.0));
1738 assert_eq!(quote.ask_price, instrument.make_price(92288.5));
1739 assert_eq!(quote.bid_size, instrument.make_qty(133440.0, None));
1740 assert_eq!(quote.ask_size, instrument.make_qty(99470.0, None));
1741 assert_eq!(quote.ts_event, UnixNanos::new(1_765_541_767_174_000_000));
1742 }
1743
1744 #[rstest]
1745 fn test_parse_book_msg_snapshot() {
1746 let instrument = test_perpetual_instrument();
1747 let json = load_test_json("ws_book_snapshot.json");
1748 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1749 let data = response["params"]["data"].to_string();
1750 let msg: DeribitBookMsg = serde_json::from_str(&data).unwrap();
1751
1752 assert_eq!(
1754 msg.bids[0].len(),
1755 3,
1756 "Snapshot bids should have 3 elements: [action, price, amount]"
1757 );
1758 assert_eq!(
1759 msg.bids[0][0].get(),
1760 r#""new""#,
1761 "First element should be 'new' action for snapshot"
1762 );
1763 assert_eq!(
1764 msg.asks[0].len(),
1765 3,
1766 "Snapshot asks should have 3 elements: [action, price, amount]"
1767 );
1768 assert_eq!(
1769 msg.asks[0][0].get(),
1770 r#""new""#,
1771 "First element should be 'new' action for snapshot"
1772 );
1773
1774 let deltas = parse_book_msg(&msg, &instrument, UnixNanos::default()).unwrap();
1775
1776 assert_eq!(deltas.instrument_id, instrument.id());
1777 assert_eq!(deltas.deltas.len(), 11);
1779
1780 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
1782
1783 let first_bid = &deltas.deltas[1];
1785 assert_eq!(first_bid.action, BookAction::Add);
1786 assert_eq!(first_bid.order.side, OrderSide::Buy.into());
1787 assert_eq!(first_bid.order.price, instrument.make_price(42500.0));
1788 assert_eq!(first_bid.order.size, instrument.make_qty(1000.0, None));
1789
1790 let first_ask = &deltas.deltas[6];
1792 assert_eq!(first_ask.action, BookAction::Add);
1793 assert_eq!(first_ask.order.side, OrderSide::Sell.into());
1794 assert_eq!(first_ask.order.price, instrument.make_price(42501.0));
1795 assert_eq!(first_ask.order.size, instrument.make_qty(800.0, None));
1796 }
1797
1798 #[rstest]
1799 fn test_parse_book_msg_delta() {
1800 let instrument = test_perpetual_instrument();
1801 let json = load_test_json("ws_book_delta.json");
1802 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1803 let data = response["params"]["data"].to_string();
1804 let msg: DeribitBookMsg = serde_json::from_str(&data).unwrap();
1805
1806 assert_eq!(
1808 msg.bids[0].len(),
1809 3,
1810 "Delta bids should have 3 elements: [action, price, amount]"
1811 );
1812 assert_eq!(
1813 msg.bids[0][0].get(),
1814 r#""change""#,
1815 "First bid should be 'change' action"
1816 );
1817 assert_eq!(
1818 msg.bids[1][0].get(),
1819 r#""new""#,
1820 "Second bid should be 'new' action"
1821 );
1822 assert_eq!(
1823 msg.asks[0].len(),
1824 3,
1825 "Delta asks should have 3 elements: [action, price, amount]"
1826 );
1827 assert_eq!(
1828 msg.asks[0][0].get(),
1829 r#""delete""#,
1830 "First ask should be 'delete' action"
1831 );
1832
1833 let deltas = parse_book_msg(&msg, &instrument, UnixNanos::default()).unwrap();
1834
1835 assert_eq!(deltas.instrument_id, instrument.id());
1836 assert_eq!(deltas.deltas.len(), 4);
1838
1839 assert_ne!(deltas.deltas[0].action, BookAction::Clear);
1841
1842 let bid_change = &deltas.deltas[0];
1844 assert_eq!(bid_change.action, BookAction::Update);
1845 assert_eq!(bid_change.order.side, OrderSide::Buy.into());
1846 assert_eq!(bid_change.order.price, instrument.make_price(42500.0));
1847 assert_eq!(bid_change.order.size, instrument.make_qty(950.0, None));
1848
1849 let bid_new = &deltas.deltas[1];
1851 assert_eq!(bid_new.action, BookAction::Add);
1852 assert_eq!(bid_new.order.side, OrderSide::Buy.into());
1853 assert_eq!(bid_new.order.price, instrument.make_price(42498.5));
1854 assert_eq!(bid_new.order.size, instrument.make_qty(300.0, None));
1855
1856 let ask_delete = &deltas.deltas[2];
1858 assert_eq!(ask_delete.action, BookAction::Delete);
1859 assert_eq!(ask_delete.order.side, OrderSide::Sell.into());
1860 assert_eq!(ask_delete.order.price, instrument.make_price(42501.0));
1861
1862 let ask_change = &deltas.deltas[3];
1864 assert_eq!(ask_change.action, BookAction::Update);
1865 assert_eq!(ask_change.order.side, OrderSide::Sell.into());
1866 assert_eq!(ask_change.order.price, instrument.make_price(42501.5));
1867 assert_eq!(ask_change.order.size, instrument.make_qty(700.0, None));
1868 }
1869
1870 #[rstest]
1871 fn test_parse_book_grouped_snapshot() {
1872 let instrument = test_perpetual_instrument();
1875 let json = load_test_json("ws_book_grouped_snapshot.json");
1876 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1877 let data = response["params"]["data"].to_string();
1878 let msg: DeribitBookMsg = serde_json::from_str(&data).unwrap();
1879
1880 assert_eq!(
1882 msg.bids[0].len(),
1883 2,
1884 "Grouped bids should have 2 elements: [price, amount]"
1885 );
1886 assert_eq!(
1887 msg.asks[0].len(),
1888 2,
1889 "Grouped asks should have 2 elements: [price, amount]"
1890 );
1891
1892 assert_eq!(
1894 msg.msg_type,
1895 DeribitBookMsgType::Snapshot,
1896 "Grouped channel should default to Snapshot type"
1897 );
1898
1899 let deltas = parse_book_snapshot(&msg, &instrument, UnixNanos::default()).unwrap();
1900
1901 assert_eq!(deltas.instrument_id, instrument.id());
1902 assert_eq!(deltas.deltas.len(), 21);
1904
1905 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
1907
1908 let first_bid = &deltas.deltas[1];
1910 assert_eq!(first_bid.action, BookAction::Add);
1911 assert_eq!(first_bid.order.side, OrderSide::Buy.into());
1912 assert_eq!(first_bid.order.price, instrument.make_price(89532.5));
1913 assert_eq!(first_bid.order.size, instrument.make_qty(254900.0, None));
1914
1915 let first_ask = &deltas.deltas[11];
1917 assert_eq!(first_ask.action, BookAction::Add);
1918 assert_eq!(first_ask.order.side, OrderSide::Sell.into());
1919 assert_eq!(first_ask.order.price, instrument.make_price(89533.0));
1920 assert_eq!(first_ask.order.size, instrument.make_qty(91570.0, None));
1921
1922 let last = deltas.deltas.last().unwrap();
1924 assert_eq!(
1925 last.flags & RecordFlag::F_LAST as u8,
1926 RecordFlag::F_LAST as u8
1927 );
1928 }
1929
1930 #[rstest]
1931 #[case::populated(true, true)]
1932 #[case::bids_only(true, false)]
1933 #[case::asks_only(false, true)]
1934 #[case::empty(false, false)]
1935 fn test_parse_book_depth(#[case] has_bids: bool, #[case] has_asks: bool) {
1936 let instrument = test_perpetual_instrument();
1937 let json = load_test_json("ws_book_grouped_snapshot.json");
1938 let mut response: serde_json::Value = serde_json::from_str(&json).unwrap();
1939 if !has_bids {
1940 response["params"]["data"]["bids"] = serde_json::json!([]);
1941 }
1942
1943 if !has_asks {
1944 response["params"]["data"]["asks"] = serde_json::json!([]);
1945 }
1946
1947 let data = response["params"]["data"].to_string();
1948 let msg: DeribitBookMsg = serde_json::from_str(&data).unwrap();
1949 let ts_init = UnixNanos::new(1_769_246_173_072_123_456);
1950 let depth = parse_book_depth(&msg, &instrument, ts_init).unwrap();
1951
1952 let bids = if has_bids {
1953 vec![
1954 ("89532.5", "254900"),
1955 ("89532.0", "1000"),
1956 ("89531.0", "2000"),
1957 ("89529.5", "3530"),
1958 ("89529.0", "4030"),
1959 ("89528.0", "500"),
1960 ("89527.5", "35170"),
1961 ("89526.0", "75010"),
1962 ("89525.0", "10000"),
1963 ("89523.5", "11000"),
1964 ]
1965 } else {
1966 vec![]
1967 };
1968
1969 let asks = if has_asks {
1970 vec![
1971 ("89533.0", "91570"),
1972 ("89533.5", "10000"),
1973 ("89537.0", "13290"),
1974 ("89538.5", "6990"),
1975 ("89540.0", "11000"),
1976 ("89540.5", "26520"),
1977 ("89541.0", "3550"),
1978 ("89542.5", "11000"),
1979 ("89543.0", "480"),
1980 ("89545.5", "29500"),
1981 ]
1982 } else {
1983 vec![]
1984 };
1985
1986 let bid_count = bids.len();
1987 let ask_count = asks.len();
1988
1989 let bids: Vec<_> = bids
1990 .into_iter()
1991 .enumerate()
1992 .map(|(i, (price, size))| {
1993 BookOrder::new(
1994 OrderSide::Buy,
1995 Price::from(price),
1996 Quantity::from(size),
1997 i as u64,
1998 )
1999 })
2000 .collect();
2001
2002 let asks: Vec<_> = asks
2003 .into_iter()
2004 .enumerate()
2005 .map(|(i, (price, size))| {
2006 BookOrder::new(
2007 OrderSide::Sell,
2008 Price::from(price),
2009 Quantity::from(size),
2010 (bid_count + i) as u64,
2011 )
2012 })
2013 .collect();
2014
2015 let expected = OrderBookDepth::new(
2016 InstrumentId::from("BTC-PERPETUAL.DERIBIT"),
2017 bids,
2018 asks,
2019 vec![0; bid_count],
2020 vec![0; ask_count],
2021 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8,
2022 136_725_664_747,
2023 UnixNanos::new(1_769_246_173_071_000_000),
2024 ts_init,
2025 );
2026
2027 assert_eq!(depth, expected);
2028 }
2029
2030 #[rstest]
2031 fn test_parse_ticker_to_mark_price() {
2032 let instrument = test_perpetual_instrument();
2033 let json = load_test_json("ws_ticker.json");
2034 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
2035 let msg: DeribitTickerMsg =
2036 serde_json::from_str(&response["params"]["data"].to_string()).unwrap();
2037
2038 let mark_price =
2039 parse_ticker_to_mark_price(&msg, &instrument, UnixNanos::default()).unwrap();
2040
2041 assert_eq!(mark_price.instrument_id, instrument.id());
2042 assert_eq!(mark_price.value, instrument.make_price(92281.78));
2043 assert_eq!(
2044 mark_price.ts_event,
2045 UnixNanos::new(1_765_541_474_086_000_000)
2046 );
2047 }
2048
2049 #[rstest]
2050 fn test_parse_ticker_to_index_price() {
2051 let instrument = test_perpetual_instrument();
2052 let json = load_test_json("ws_ticker.json");
2053 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
2054 let msg: DeribitTickerMsg =
2055 serde_json::from_str(&response["params"]["data"].to_string()).unwrap();
2056
2057 let index_price =
2058 parse_ticker_to_index_price(&msg, &instrument, UnixNanos::default()).unwrap();
2059
2060 assert_eq!(index_price.instrument_id, instrument.id());
2061 assert_eq!(index_price.value, instrument.make_price(92263.55));
2062 assert_eq!(
2063 index_price.ts_event,
2064 UnixNanos::new(1_765_541_474_086_000_000)
2065 );
2066 }
2067
2068 #[rstest]
2069 #[case::all_present(Some(dec!(55.2)), Some(dec!(54.0)), Some(dec!(56.5)))]
2070 #[case::missing_mark(None, Some(dec!(54.0)), Some(dec!(56.5)))]
2071 #[case::missing_bid(Some(dec!(55.2)), None, Some(dec!(56.5)))]
2072 #[case::missing_ask(Some(dec!(55.2)), Some(dec!(54.0)), None)]
2073 #[case::all_missing(None, None, None)]
2074 fn test_parse_ticker_to_option_greeks(
2075 option_ticker: (DeribitTickerMsg, InstrumentAny),
2076 #[case] mark_iv: Option<Decimal>,
2077 #[case] bid_iv: Option<Decimal>,
2078 #[case] ask_iv: Option<Decimal>,
2079 ) {
2080 let (mut msg, instrument) = option_ticker;
2081 msg.mark_iv = mark_iv;
2082 msg.bid_iv = bid_iv;
2083 msg.ask_iv = ask_iv;
2084 let ts_init = UnixNanos::new(1_765_541_474_087_000_000);
2085
2086 let greeks = parse_ticker_to_option_greeks(&msg, &instrument, ts_init).unwrap();
2087
2088 assert_eq!(
2089 greeks.instrument_id,
2090 InstrumentId::from("BTC-27DEC24-100000-C.DERIBIT"),
2091 );
2092 assert_eq!(greeks.convention, GreeksConvention::BlackScholes);
2093 assert_eq!(
2094 greeks.greeks,
2095 OptionGreekValues {
2096 delta: 0.45,
2097 gamma: 0.00012,
2098 vega: 14.25,
2099 theta: -23.5,
2100 rho: 8.75,
2101 },
2102 );
2103
2104 for (actual, expected) in [
2105 (greeks.mark_iv, mark_iv.map(|_| 0.552)),
2106 (greeks.bid_iv, bid_iv.map(|_| 0.54)),
2107 (greeks.ask_iv, ask_iv.map(|_| 0.565)),
2108 ] {
2109 match expected {
2110 Some(expected) => assert!((actual.unwrap() - expected).abs() < 1e-12),
2111 None => assert_eq!(actual, None),
2112 }
2113 }
2114
2115 assert_eq!(greeks.underlying_price, Some(95000.5));
2116 assert_eq!(greeks.open_interest, Some(123.5));
2117 assert_eq!(greeks.ts_event, UnixNanos::new(1_765_541_474_086_000_000));
2118 assert_eq!(greeks.ts_init, ts_init);
2119 assert_eq!(msg.mark_iv, mark_iv);
2120 assert_eq!(msg.bid_iv, bid_iv);
2121 assert_eq!(msg.ask_iv, ask_iv);
2122 }
2123
2124 #[rstest]
2125 fn test_parse_ticker_to_option_greeks_without_greeks(
2126 option_ticker: (DeribitTickerMsg, InstrumentAny),
2127 ) {
2128 let (mut msg, instrument) = option_ticker;
2129 msg.greeks = None;
2130
2131 assert_eq!(
2132 parse_ticker_to_option_greeks(&msg, &instrument, UnixNanos::default()),
2133 None,
2134 );
2135 }
2136
2137 #[fixture]
2138 fn option_ticker() -> (DeribitTickerMsg, InstrumentAny) {
2139 let json = load_test_json("http_get_instrument_option.json");
2140 let response: DeribitJsonRpcResponse<DeribitInstrument> =
2141 serde_json::from_str(&json).unwrap();
2142 let instrument = parse_deribit_instrument_any(
2143 &response.result.unwrap(),
2144 UnixNanos::default(),
2145 UnixNanos::default(),
2146 )
2147 .unwrap()
2148 .unwrap();
2149 let json = load_test_json("ws_ticker_option.json");
2150 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
2151 let msg = serde_json::from_str(&response["params"]["data"].to_string()).unwrap();
2152 (msg, instrument)
2153 }
2154
2155 #[rstest]
2156 fn test_parse_ticker_to_funding_rate() {
2157 let instrument = test_perpetual_instrument();
2158 let json = load_test_json("ws_ticker.json");
2159 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
2160 let msg: DeribitTickerMsg =
2161 serde_json::from_str(&response["params"]["data"].to_string()).unwrap();
2162
2163 assert!(msg.current_funding.is_some());
2165
2166 let funding_rate =
2167 parse_ticker_to_funding_rate(&msg, &instrument, UnixNanos::default()).unwrap();
2168
2169 assert_eq!(funding_rate.instrument_id, instrument.id());
2170 assert_eq!(
2172 funding_rate.ts_event,
2173 UnixNanos::new(1_765_541_474_086_000_000)
2174 );
2175 assert!(funding_rate.interval.is_none());
2176 assert!(funding_rate.next_funding_ns.is_none()); }
2178
2179 #[rstest]
2180 fn test_resolution_to_bar_type_1_minute() {
2181 let instrument_id = InstrumentId::from("BTC-PERPETUAL.DERIBIT");
2182 let bar_type = resolution_to_bar_type(instrument_id, "1").unwrap();
2183
2184 assert_eq!(bar_type.instrument_id(), instrument_id);
2185 assert_eq!(bar_type.spec().step.get(), 1);
2186 assert_eq!(bar_type.spec().aggregation, BarAggregation::Minute);
2187 assert_eq!(bar_type.spec().price_type, PriceType::Last);
2188 assert_eq!(bar_type.aggregation_source(), AggregationSource::External);
2189 }
2190
2191 #[rstest]
2192 fn test_resolution_to_bar_type_60_minute() {
2193 let instrument_id = InstrumentId::from("ETH-PERPETUAL.DERIBIT");
2194 let bar_type = resolution_to_bar_type(instrument_id, "60").unwrap();
2195
2196 assert_eq!(bar_type.instrument_id(), instrument_id);
2197 assert_eq!(bar_type.spec().step.get(), 1);
2198 assert_eq!(bar_type.spec().aggregation, BarAggregation::Hour);
2199 }
2200
2201 #[rstest]
2202 fn test_resolution_to_bar_type_daily() {
2203 let instrument_id = InstrumentId::from("BTC-PERPETUAL.DERIBIT");
2204 let bar_type = resolution_to_bar_type(instrument_id, "1D").unwrap();
2205
2206 assert_eq!(bar_type.instrument_id(), instrument_id);
2207 assert_eq!(bar_type.spec().step.get(), 1);
2208 assert_eq!(bar_type.spec().aggregation, BarAggregation::Day);
2209 }
2210
2211 #[rstest]
2212 fn test_resolution_to_bar_type_invalid() {
2213 let instrument_id = InstrumentId::from("BTC-PERPETUAL.DERIBIT");
2214 let result = resolution_to_bar_type(instrument_id, "invalid");
2215
2216 assert!(result.is_err());
2217 assert!(
2218 result
2219 .unwrap_err()
2220 .to_string()
2221 .contains("Unsupported Deribit resolution")
2222 );
2223 }
2224
2225 #[rstest]
2226 fn test_parse_chart_msg_uses_cost() {
2227 let instrument = test_perpetual_instrument();
2228 assert!(
2229 instrument.is_inverse(),
2230 "test fixture is expected to be an inverse perp"
2231 );
2232
2233 let json = load_test_json("ws_chart.json");
2234 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
2235 let chart_msg: DeribitChartMsg =
2236 serde_json::from_str(&response["params"]["data"].to_string()).unwrap();
2237
2238 assert_eq!(chart_msg.tick, 1_767_200_040_000);
2240 assert_eq!(chart_msg.open, dec!(87490.0));
2241 assert_eq!(chart_msg.high, dec!(87500.0));
2242 assert_eq!(chart_msg.low, dec!(87465.0));
2243 assert_eq!(chart_msg.close, dec!(87474.0));
2244 assert_eq!(chart_msg.volume, dec!(0.95978896));
2245 assert_eq!(chart_msg.cost, dec!(83970.0));
2246
2247 let bar_type = resolution_to_bar_type(instrument.id(), "1").unwrap();
2248
2249 let bar = parse_chart_msg(
2251 &chart_msg,
2252 bar_type,
2253 instrument.price_precision(),
2254 instrument.size_precision(),
2255 true, true,
2257 UnixNanos::default(),
2258 )
2259 .unwrap();
2260
2261 assert_eq!(bar.bar_type, bar_type);
2262 assert_eq!(bar.open, instrument.make_price(87490.0));
2263 assert_eq!(bar.high, instrument.make_price(87500.0));
2264 assert_eq!(bar.low, instrument.make_price(87465.0));
2265 assert_eq!(bar.close, instrument.make_price(87474.0));
2266 assert_eq!(bar.volume, instrument.make_qty(83970.0, None));
2267
2268 assert_eq!(bar.ts_event, UnixNanos::new(1_767_200_100_000_000_000));
2270 }
2271
2272 #[rstest]
2273 fn test_parse_order_buy_response() {
2274 let instrument = test_perpetual_instrument();
2275 let json = load_test_json("ws_order_buy_response.json");
2276 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
2277
2278 let order_msg: DeribitOrderMsg =
2280 serde_json::from_str(&response["result"]["order"].to_string()).unwrap();
2281
2282 assert_eq!(order_msg.order_id, "USDC-104819327443");
2284 assert_eq!(
2285 order_msg.label,
2286 Some("O-19700101-000000-001-001-1".to_string())
2287 );
2288 assert_eq!(order_msg.direction, "buy");
2289 assert_eq!(order_msg.order_state, "open");
2290 assert_eq!(order_msg.order_type, "limit");
2291 assert_eq!(order_msg.price, Some(dec!(2973.55)));
2292 assert_eq!(order_msg.amount, dec!(0.001));
2293 assert_eq!(order_msg.filled_amount, rust_decimal::Decimal::ZERO);
2294 assert!(order_msg.post_only);
2295 assert!(!order_msg.reduce_only);
2296
2297 let account_id = AccountId::new("DERIBIT-001");
2299 let trader_id = TraderId::new("TRADER-001");
2300 let strategy_id = StrategyId::new("PMM-001");
2301
2302 let accepted = parse_order_accepted(
2303 &order_msg,
2304 &instrument,
2305 account_id,
2306 trader_id,
2307 strategy_id,
2308 UnixNanos::default(),
2309 );
2310
2311 assert_eq!(
2312 accepted.client_order_id.to_string(),
2313 "O-19700101-000000-001-001-1"
2314 );
2315 assert_eq!(accepted.venue_order_id.to_string(), "USDC-104819327443");
2316 assert_eq!(accepted.trader_id, trader_id);
2317 assert_eq!(accepted.strategy_id, strategy_id);
2318 assert_eq!(accepted.account_id, account_id);
2319 }
2320
2321 #[rstest]
2322 fn test_parse_order_sell_response() {
2323 let instrument = test_perpetual_instrument();
2324 let json = load_test_json("ws_order_sell_response.json");
2325 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
2326
2327 let order_msg: DeribitOrderMsg =
2328 serde_json::from_str(&response["result"]["order"].to_string()).unwrap();
2329
2330 assert_eq!(order_msg.order_id, "USDC-104819327458");
2332 assert_eq!(
2333 order_msg.label,
2334 Some("O-19700101-000000-001-001-2".to_string())
2335 );
2336 assert_eq!(order_msg.direction, "sell");
2337 assert_eq!(order_msg.order_state, "open");
2338 assert_eq!(order_msg.price, Some(dec!(3286.7)));
2339 assert_eq!(order_msg.amount, dec!(0.001));
2340
2341 let account_id = AccountId::new("DERIBIT-001");
2343 let trader_id = TraderId::new("TRADER-001");
2344 let strategy_id = StrategyId::new("PMM-001");
2345
2346 let accepted = parse_order_accepted(
2347 &order_msg,
2348 &instrument,
2349 account_id,
2350 trader_id,
2351 strategy_id,
2352 UnixNanos::default(),
2353 );
2354
2355 assert_eq!(
2356 accepted.client_order_id.to_string(),
2357 "O-19700101-000000-001-001-2"
2358 );
2359 assert_eq!(accepted.venue_order_id.to_string(), "USDC-104819327458");
2360 assert_eq!(accepted.trader_id, trader_id);
2361 assert_eq!(accepted.strategy_id, strategy_id);
2362 assert_eq!(accepted.account_id, account_id);
2363 }
2364
2365 #[rstest]
2366 fn test_parse_order_edit_response() {
2367 let instrument = test_perpetual_instrument();
2368 let json = load_test_json("ws_order_edit_response.json");
2369 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
2370
2371 let order_msg: DeribitOrderMsg =
2372 serde_json::from_str(&response["result"]["order"].to_string()).unwrap();
2373
2374 assert_eq!(order_msg.order_id, "USDC-104819327443");
2376 assert_eq!(
2377 order_msg.label,
2378 Some("O-19700101-000000-001-001-1".to_string())
2379 );
2380 assert_eq!(order_msg.direction, "buy");
2381 assert_eq!(order_msg.order_state, "open");
2382 assert!(order_msg.replaced);
2383 assert_eq!(order_msg.price, Some(dec!(3067.2))); let account_id = AccountId::new("DERIBIT-001");
2387 let trader_id = TraderId::new("TRADER-001");
2388 let strategy_id = StrategyId::new("PMM-001");
2389
2390 let updated = parse_order_updated(
2391 &order_msg,
2392 &instrument,
2393 account_id,
2394 trader_id,
2395 strategy_id,
2396 UnixNanos::default(),
2397 );
2398
2399 assert_eq!(
2400 updated.client_order_id.to_string(),
2401 "O-19700101-000000-001-001-1"
2402 );
2403 assert_eq!(
2404 updated.venue_order_id.unwrap().to_string(),
2405 "USDC-104819327443"
2406 );
2407 assert_eq!(updated.quantity.as_f64(), 0.0);
2409 }
2410
2411 #[rstest]
2412 fn test_parse_order_cancel_response() {
2413 let instrument = test_perpetual_instrument();
2414 let json = load_test_json("ws_order_cancel_response.json");
2415 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
2416
2417 let order_msg: DeribitOrderMsg =
2419 serde_json::from_str(&response["result"].to_string()).unwrap();
2420
2421 assert_eq!(order_msg.order_id, "USDC-104819327443");
2423 assert_eq!(
2424 order_msg.label,
2425 Some("O-19700101-000000-001-001-1".to_string())
2426 );
2427 assert_eq!(order_msg.order_state, "cancelled");
2428 assert_eq!(order_msg.cancel_reason, Some("user_request".to_string()));
2429
2430 let account_id = AccountId::new("DERIBIT-001");
2432 let trader_id = TraderId::new("TRADER-001");
2433 let strategy_id = StrategyId::new("PMM-001");
2434
2435 let canceled = parse_order_canceled(
2436 &order_msg,
2437 &instrument,
2438 account_id,
2439 trader_id,
2440 strategy_id,
2441 UnixNanos::default(),
2442 );
2443
2444 assert_eq!(
2445 canceled.client_order_id.to_string(),
2446 "O-19700101-000000-001-001-1"
2447 );
2448 assert_eq!(
2449 canceled.venue_order_id.unwrap().to_string(),
2450 "USDC-104819327443"
2451 );
2452 assert_eq!(canceled.trader_id, trader_id);
2453 assert_eq!(canceled.strategy_id, strategy_id);
2454 }
2455
2456 #[rstest]
2457 fn test_parse_order_stop_market_response() {
2458 let instrument = test_perpetual_instrument();
2463 let json = load_test_json("ws_order_stop_market_response.json");
2464 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
2465
2466 let order_msg: DeribitOrderMsg =
2467 serde_json::from_str(&response["result"]["order"].to_string()).unwrap();
2468
2469 assert_eq!(order_msg.order_id, "USDC-104819327499");
2470 assert_eq!(order_msg.order_type, "stop_market");
2471 assert_eq!(order_msg.order_state, "untriggered");
2472 assert_eq!(order_msg.price, None);
2473 assert_eq!(order_msg.trigger_price, Some(dec!(2228.0)));
2474 assert_eq!(order_msg.trigger.as_deref(), Some("mark_price"));
2475 assert!(order_msg.reduce_only);
2476
2477 let account_id = AccountId::new("DERIBIT-001");
2478 let report =
2479 parse_user_order_msg(&order_msg, &instrument, account_id, UnixNanos::default())
2480 .unwrap();
2481
2482 assert_eq!(report.order_type, OrderType::StopMarket);
2483 assert_eq!(report.order_status, OrderStatus::Accepted);
2484 assert!(report.price.is_none());
2485 assert!(report.trigger_price.is_some());
2486 assert!(report.reduce_only);
2487 }
2488
2489 #[rstest]
2490 fn test_parse_order_stop_market_response_missing_filled_amount() {
2491 let instrument = test_perpetual_instrument();
2496 let json = load_test_json("ws_order_stop_market_no_filled_amount.json");
2497 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
2498
2499 let order_msg: DeribitOrderMsg =
2500 serde_json::from_str(&response["result"]["order"].to_string()).unwrap();
2501
2502 assert_eq!(order_msg.order_id, "USDC-SLMB-19641");
2503 assert_eq!(order_msg.order_type, "stop_market");
2504 assert_eq!(order_msg.order_state, "untriggered");
2505 assert_eq!(order_msg.filled_amount, rust_decimal::Decimal::ZERO);
2506 assert_eq!(order_msg.average_price, None);
2507
2508 let account_id = AccountId::new("DERIBIT-001");
2509 let report =
2510 parse_user_order_msg(&order_msg, &instrument, account_id, UnixNanos::default())
2511 .unwrap();
2512
2513 assert_eq!(report.order_type, OrderType::StopMarket);
2514 assert_eq!(report.order_status, OrderStatus::Accepted);
2515 assert_eq!(report.filled_qty.as_f64(), 0.0);
2516 }
2517
2518 #[rstest]
2519 fn test_parse_user_order_msg_to_status_report() {
2520 let instrument = test_perpetual_instrument();
2521 let json = load_test_json("ws_order_buy_response.json");
2522 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
2523
2524 let order_msg: DeribitOrderMsg =
2525 serde_json::from_str(&response["result"]["order"].to_string()).unwrap();
2526
2527 let account_id = AccountId::new("DERIBIT-001");
2528 let report =
2529 parse_user_order_msg(&order_msg, &instrument, account_id, UnixNanos::default())
2530 .unwrap();
2531
2532 assert_eq!(report.venue_order_id.to_string(), "USDC-104819327443");
2533 assert_eq!(
2534 report.client_order_id.unwrap().to_string(),
2535 "O-19700101-000000-001-001-1"
2536 );
2537 assert_eq!(report.order_side, OrderSide::Buy.into());
2538 assert_eq!(report.order_type, OrderType::Limit);
2539 assert_eq!(report.time_in_force, TimeInForce::Gtc);
2540 assert_eq!(report.order_status, OrderStatus::Accepted);
2541 assert_eq!(report.quantity.as_f64(), 0.0);
2543 assert_eq!(report.filled_qty.as_f64(), 0.0);
2544 assert!(report.post_only);
2545 assert!(!report.reduce_only);
2546 }
2547
2548 #[rstest]
2549 fn test_determine_order_event_type() {
2550 assert_eq!(
2552 determine_order_event_type("open", true, false),
2553 OrderEventType::Accepted
2554 );
2555
2556 assert_eq!(
2558 determine_order_event_type("open", false, true),
2559 OrderEventType::Updated
2560 );
2561
2562 assert_eq!(
2564 determine_order_event_type("cancelled", false, false),
2565 OrderEventType::Canceled
2566 );
2567
2568 assert_eq!(
2570 determine_order_event_type("expired", false, false),
2571 OrderEventType::Expired
2572 );
2573
2574 assert_eq!(
2576 determine_order_event_type("filled", false, false),
2577 OrderEventType::None
2578 );
2579 }
2580
2581 fn perpetual_instrument_with_increments(
2582 tick_size: &str,
2583 min_trade_amount: &str,
2584 ) -> InstrumentAny {
2585 let json = load_test_json("http_get_instruments.json");
2586 let mut response: serde_json::Value = serde_json::from_str(&json).unwrap();
2587 response["result"][0]["tick_size"] = serde_json::json!(tick_size);
2588 response["result"][0]["min_trade_amount"] = serde_json::json!(min_trade_amount);
2589 let response: DeribitJsonRpcResponse<Vec<DeribitInstrument>> =
2590 serde_json::from_str(&response.to_string()).unwrap();
2591 let instrument = &response.result.unwrap()[0];
2592 parse_deribit_instrument_any(instrument, UnixNanos::default(), UnixNanos::default())
2593 .unwrap()
2594 .unwrap()
2595 }
2596
2597 fn parse_notification_data<T: DeserializeOwned>(fixture: &str) -> T {
2598 serde_json::from_str(¬ification_data(fixture)).unwrap()
2599 }
2600
2601 fn notification_data(fixture: &str) -> String {
2602 let json = load_test_json(fixture);
2603
2604 let DeribitWsMessage::Notification(notification) = parse_raw_message(&json).unwrap() else {
2605 panic!("Expected notification from {fixture}");
2606 };
2607
2608 notification.params.data.get().to_string()
2609 }
2610
2611 #[rstest]
2612 fn test_parse_user_trade_msg_keeps_decimals_that_collapse_in_f64() {
2613 let instrument = perpetual_instrument_with_increments("0.000000001", "0.000000001");
2614 let trades: Vec<DeribitUserTradeMsg> = parse_notification_data("ws_user_trades_exact.json");
2615 let account_id = AccountId::new("DERIBIT-001");
2616 let btc = Currency::BTC();
2617
2618 let first = parse_user_trade_msg(&trades[0], &instrument, account_id, UnixNanos::default())
2619 .unwrap();
2620 let second =
2621 parse_user_trade_msg(&trades[1], &instrument, account_id, UnixNanos::default())
2622 .unwrap();
2623
2624 let fee_f64 = "1000000000.12345678".parse::<f64>().unwrap();
2625 let next_fee_f64 = "1000000000.12345679".parse::<f64>().unwrap();
2626 assert_eq!(fee_f64.to_bits(), next_fee_f64.to_bits());
2627 assert_eq!(instrument.price_precision(), 9);
2628 assert_eq!(instrument.size_precision(), 9);
2629 assert_eq!(first.last_px, Price::from("100000000.123456789"));
2630 assert_eq!(first.last_qty, Quantity::from("100000000.123456789"));
2631 assert_eq!(
2632 first.commission,
2633 Money::from_decimal(dec!(1000000000.12345678), btc).unwrap()
2634 );
2635 assert_eq!(second.last_px, Price::from("100000000.123456788"));
2636 assert_eq!(second.last_qty, Quantity::from("100000000.123456788"));
2637 assert_eq!(
2638 second.commission,
2639 Money::from_decimal(dec!(1000000000.12345679), btc).unwrap()
2640 );
2641 assert_eq!(trades[0].mark_price, dec!(100000000.123456788));
2642 assert_eq!(trades[0].profit_loss, Some(dec!(0.30000000000000004)));
2643 }
2644
2645 #[rstest]
2646 fn test_parse_user_trade_msg_rounds_exact_token_half_even() {
2647 let instrument = perpetual_instrument_with_increments("0.01", "0.01");
2648 let trades: Vec<DeribitUserTradeMsg> = parse_notification_data("ws_user_trades_exact.json");
2649 let account_id = AccountId::new("DERIBIT-001");
2650
2651 let fill = parse_user_trade_msg(&trades[2], &instrument, account_id, UnixNanos::default())
2652 .unwrap();
2653
2654 let token_f64 = "100000000.005000001".parse::<f64>().unwrap();
2655 let via_f64 = Price::from_decimal_dp(Decimal::try_from(token_f64).unwrap(), 2).unwrap();
2656 assert_eq!(via_f64, Price::from("100000000.00"));
2657 assert_eq!(fill.last_px, Price::from("100000000.01"));
2658 assert_eq!(fill.last_qty, Quantity::from("100000000.01"));
2659 assert_eq!(
2660 fill.commission,
2661 Money::from_decimal(dec!(0.00011403), Currency::BTC()).unwrap()
2662 );
2663 }
2664
2665 #[rstest]
2666 fn test_parse_portfolio_keeps_decimals_that_collapse_in_f64() {
2667 let portfolio: DeribitPortfolioMsg = parse_notification_data("ws_portfolio_exact.json");
2668 let account_id = AccountId::new("DERIBIT-001");
2669 let btc = Currency::BTC();
2670
2671 let state =
2672 parse_portfolio_to_account_state(&portfolio, account_id, UnixNanos::default()).unwrap();
2673
2674 assert_eq!(state.balances.len(), 1);
2675 assert_eq!(
2676 state.balances[0].total,
2677 Money::from_decimal(dec!(1000000000.12345679), btc).unwrap()
2678 );
2679 assert_eq!(
2680 state.balances[0].free,
2681 Money::from_decimal(dec!(1000000000.12345678), btc).unwrap()
2682 );
2683 assert_eq!(
2684 state.balances[0].locked,
2685 Money::from_decimal(dec!(0.00000001), btc).unwrap()
2686 );
2687 assert_eq!(state.margins.len(), 1);
2688 assert_eq!(
2689 state.margins[0].initial,
2690 Money::from_decimal(dec!(1000000000.12345678), btc).unwrap()
2691 );
2692 assert_eq!(state.margins[0].maintenance, Money::zero(btc));
2693 assert_eq!(
2694 portfolio.maintenance_margin,
2695 dec!(0.0000000000000000555111512313)
2696 );
2697 }
2698
2699 #[rstest]
2700 fn test_parse_book_snapshot_keeps_decimals_that_collapse_in_f64() {
2701 let instrument = perpetual_instrument_with_increments("0.000000001", "0.000000001");
2702 let data = notification_data("ws_book_snapshot_exact.json");
2703 let msg: DeribitBookMsg = serde_json::from_str(&data).unwrap();
2704
2705 let deltas = parse_book_msg(&msg, &instrument, UnixNanos::default()).unwrap();
2706
2707 assert_eq!(deltas.deltas.len(), 3);
2708 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
2709 assert_eq!(
2710 deltas.deltas[1].order.price,
2711 Price::from("100000000.123456789")
2712 );
2713 assert_eq!(
2714 deltas.deltas[1].order.size,
2715 Quantity::from("100000000.123456789")
2716 );
2717 assert_eq!(
2718 deltas.deltas[2].order.price,
2719 Price::from("100000000.123456788")
2720 );
2721 assert_eq!(
2722 deltas.deltas[2].order.size,
2723 Quantity::from("100000000.123456788")
2724 );
2725 }
2726
2727 #[rstest]
2728 fn test_parse_book_delta_rounds_levels_beyond_decimal_scale() {
2729 let instrument = perpetual_instrument_with_increments("0.000000001", "0.000000001");
2730 let data = notification_data("ws_book_delta_exact.json");
2731 let msg: DeribitBookMsg = serde_json::from_str(&data).unwrap();
2732
2733 let deltas = parse_book_msg(&msg, &instrument, UnixNanos::default()).unwrap();
2734
2735 assert_eq!(deltas.deltas.len(), 2);
2736 assert_eq!(deltas.deltas[0].action, BookAction::Update);
2737 assert_eq!(
2738 deltas.deltas[0].order.price,
2739 Price::from("100000000.123456789")
2740 );
2741 assert_eq!(deltas.deltas[0].order.size, Quantity::from("0.123456789"));
2742 assert_eq!(deltas.deltas[1].action, BookAction::Add);
2743 assert_eq!(
2744 deltas.deltas[1].order.price,
2745 Price::from("100000000.123456788")
2746 );
2747 assert_eq!(deltas.deltas[1].order.size, Quantity::from("0.000012346"));
2748 }
2749
2750 #[rstest]
2751 fn test_parse_book_delta_maps_zero_rounding_amounts_to_delete() {
2752 let instrument = perpetual_instrument_with_increments("0.5", "1");
2753 let data = r#"{"type":"change","instrument_name":"BTC-PERPETUAL","timestamp":1700000000000,
2754 "change_id":101,"prev_change_id":100,
2755 "bids":[["change",42500.0,1.1102230246251565e-16],["new",42499.5,0.4]],
2756 "asks":[["change",42500.5,20.0],["delete",42501.0,0.0]]}"#;
2757 let msg: DeribitBookMsg = serde_json::from_str(data).unwrap();
2758
2759 let deltas = parse_book_msg(&msg, &instrument, UnixNanos::default()).unwrap();
2760
2761 let actual: Vec<_> = deltas
2762 .deltas
2763 .iter()
2764 .map(|delta| {
2765 (
2766 delta.action,
2767 delta.order.side,
2768 delta.order.price,
2769 delta.order.size,
2770 delta.order.order_id,
2771 delta.flags,
2772 delta.sequence,
2773 )
2774 })
2775 .collect();
2776
2777 assert_eq!(
2778 actual,
2779 vec![
2780 (
2781 BookAction::Delete,
2782 Some(OrderSide::Buy),
2783 Price::from("42500.0"),
2784 Quantity::from("0"),
2785 0,
2786 0,
2787 101,
2788 ),
2789 (
2790 BookAction::Delete,
2791 Some(OrderSide::Buy),
2792 Price::from("42499.5"),
2793 Quantity::from("0"),
2794 1,
2795 0,
2796 101,
2797 ),
2798 (
2799 BookAction::Update,
2800 Some(OrderSide::Sell),
2801 Price::from("42500.5"),
2802 Quantity::from("20"),
2803 2,
2804 0,
2805 101,
2806 ),
2807 (
2808 BookAction::Delete,
2809 Some(OrderSide::Sell),
2810 Price::from("42501.0"),
2811 Quantity::from("0"),
2812 3,
2813 RecordFlag::F_LAST as u8,
2814 101,
2815 ),
2816 ]
2817 );
2818 }
2819
2820 #[rstest]
2821 fn test_parse_book_snapshot_skips_zero_rounding_amounts() {
2822 let instrument = perpetual_instrument_with_increments("0.5", "1");
2823 let data = r#"{"type":"snapshot","instrument_name":"BTC-PERPETUAL","timestamp":1700000000000,
2824 "change_id":100,
2825 "bids":[["new",42500.0,1.1102230246251565e-16],["new",42499.5,10.0]],
2826 "asks":[["new",42500.5,0.4]]}"#;
2827 let msg: DeribitBookMsg = serde_json::from_str(data).unwrap();
2828
2829 let deltas = parse_book_msg(&msg, &instrument, UnixNanos::default()).unwrap();
2830
2831 let actual: Vec<_> = deltas
2832 .deltas
2833 .iter()
2834 .map(|delta| {
2835 (
2836 delta.action,
2837 delta.order.side,
2838 delta.order.price,
2839 delta.order.size,
2840 delta.order.order_id,
2841 delta.flags,
2842 )
2843 })
2844 .collect();
2845
2846 assert_eq!(
2847 actual,
2848 vec![
2849 (
2850 BookAction::Clear,
2851 None,
2852 Price::from("0"),
2853 Quantity::from("0"),
2854 0,
2855 RecordFlag::F_SNAPSHOT as u8,
2856 ),
2857 (
2858 BookAction::Add,
2859 Some(OrderSide::Buy),
2860 Price::from("42499.5"),
2861 Quantity::from("10"),
2862 1,
2863 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8,
2864 ),
2865 ]
2866 );
2867 }
2868
2869 #[rstest]
2870 #[case::snapshot(
2871 r#"{"type":"snapshot","instrument_name":"BTC-PERPETUAL","timestamp":1700000000000,
2872 "change_id":100,"bids":[["new",100000000000000000000.0,10.0]],"asks":[]}"#
2873 )]
2874 #[case::delta(
2875 r#"{"type":"change","instrument_name":"BTC-PERPETUAL","timestamp":1700000000000,
2876 "change_id":101,"prev_change_id":100,
2877 "bids":[["new",100000000000000000000.0,10.0]],"asks":[]}"#
2878 )]
2879 fn test_parse_book_msg_rejects_unconvertible_price(#[case] data: &str) {
2880 let instrument = perpetual_instrument_with_increments("0.5", "1");
2881 let msg: DeribitBookMsg = serde_json::from_str(data).unwrap();
2882
2883 let error = parse_book_msg(&msg, &instrument, UnixNanos::default()).unwrap_err();
2884
2885 let expected = Price::from_decimal_dp(dec!(100000000000000000000.0), 1).unwrap_err();
2886 assert_eq!(error.to_string(), expected.to_string());
2887 }
2888}