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, QuoteTick, TradeTick, bar::BarSpecification,
29 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
433fn is_zero_at_size_precision(
436 size: Quantity,
437 amount: Decimal,
438 side: &str,
439 index: usize,
440 instrument_name: &str,
441) -> bool {
442 if size.is_positive() {
443 return false;
444 }
445
446 log::warn!(
447 "Book {side} amount {amount} at index {index} for {instrument_name} is zero at size precision {}, treating level as absent",
448 size.precision
449 );
450 true
451}
452
453pub fn parse_ticker_to_quote(
459 msg: &DeribitTickerMsg,
460 instrument: &InstrumentAny,
461 ts_init: UnixNanos,
462) -> anyhow::Result<QuoteTick> {
463 let instrument_id = instrument.id();
464 let price_precision = instrument.price_precision();
465 let size_precision = instrument.size_precision();
466
467 let bid_price_val = msg
468 .best_bid_price
469 .context("Missing best_bid_price in ticker")?;
470 let ask_price_val = msg
471 .best_ask_price
472 .context("Missing best_ask_price in ticker")?;
473
474 let bid_price = Price::from_decimal_dp(bid_price_val, price_precision)?;
475 let ask_price = Price::from_decimal_dp(ask_price_val, price_precision)?;
476 let bid_size =
477 Quantity::from_decimal_dp(msg.best_bid_amount.unwrap_or_default(), size_precision)?;
478 let ask_size =
479 Quantity::from_decimal_dp(msg.best_ask_amount.unwrap_or_default(), size_precision)?;
480 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
481
482 QuoteTick::new_checked(
483 instrument_id,
484 bid_price,
485 ask_price,
486 bid_size,
487 ask_size,
488 ts_event,
489 ts_init,
490 )
491}
492
493pub fn parse_quote_msg(
499 msg: &DeribitQuoteMsg,
500 instrument: &InstrumentAny,
501 ts_init: UnixNanos,
502) -> anyhow::Result<QuoteTick> {
503 let instrument_id = instrument.id();
504 let price_precision = instrument.price_precision();
505 let size_precision = instrument.size_precision();
506
507 let bid_price = Price::from_decimal_dp(msg.best_bid_price, price_precision)?;
508 let ask_price = Price::from_decimal_dp(msg.best_ask_price, price_precision)?;
509 let bid_size = Quantity::from_decimal_dp(msg.best_bid_amount, size_precision)?;
510 let ask_size = Quantity::from_decimal_dp(msg.best_ask_amount, size_precision)?;
511 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
512
513 QuoteTick::new_checked(
514 instrument_id,
515 bid_price,
516 ask_price,
517 bid_size,
518 ask_size,
519 ts_event,
520 ts_init,
521 )
522}
523
524pub fn parse_ticker_to_mark_price(
530 msg: &DeribitTickerMsg,
531 instrument: &InstrumentAny,
532 ts_init: UnixNanos,
533) -> anyhow::Result<MarkPriceUpdate> {
534 let instrument_id = instrument.id();
535 let price_precision = instrument.price_precision();
536 let value = Price::from_decimal_dp(msg.mark_price, price_precision)?;
537 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
538
539 Ok(MarkPriceUpdate::new(
540 instrument_id,
541 value,
542 ts_event,
543 ts_init,
544 ))
545}
546
547pub fn parse_ticker_to_index_price(
553 msg: &DeribitTickerMsg,
554 instrument: &InstrumentAny,
555 ts_init: UnixNanos,
556) -> anyhow::Result<IndexPriceUpdate> {
557 let instrument_id = instrument.id();
558 let price_precision = instrument.price_precision();
559 let value = Price::from_decimal_dp(msg.index_price, price_precision)?;
560 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
561
562 Ok(IndexPriceUpdate::new(
563 instrument_id,
564 value,
565 ts_event,
566 ts_init,
567 ))
568}
569
570#[must_use]
574pub fn parse_ticker_to_funding_rate(
575 msg: &DeribitTickerMsg,
576 instrument: &InstrumentAny,
577 ts_init: UnixNanos,
578) -> Option<FundingRateUpdate> {
579 let rate = msg.current_funding?;
581 let instrument_id = instrument.id();
582 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
583
584 Some(FundingRateUpdate::new(
586 instrument_id,
587 rate,
588 None, None, ts_event,
591 ts_init,
592 ))
593}
594
595#[must_use]
599pub fn parse_ticker_to_option_greeks(
600 msg: &DeribitTickerMsg,
601 instrument: &InstrumentAny,
602 ts_init: UnixNanos,
603) -> Option<OptionGreeks> {
604 let deribit_greeks = msg.greeks.as_ref()?;
605 let instrument_id = instrument.id();
606 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
607
608 Some(OptionGreeks {
609 instrument_id,
610 convention: GreeksConvention::BlackScholes,
611 greeks: deribit_greeks.to_greek_values(),
612 mark_iv: msg.mark_iv.and_then(|v| v.to_f64()),
613 bid_iv: msg.bid_iv.and_then(|v| v.to_f64()),
614 ask_iv: msg.ask_iv.and_then(|v| v.to_f64()),
615 underlying_price: msg.underlying_price.and_then(|v| v.to_f64()),
616 open_interest: Some(msg.open_interest.to_f64().unwrap_or(0.0)),
617 ts_event,
618 ts_init,
619 })
620}
621
622#[must_use]
627pub fn parse_perpetual_to_funding_rate(
628 msg: &DeribitPerpetualMsg,
629 instrument: &InstrumentAny,
630 ts_init: UnixNanos,
631) -> FundingRateUpdate {
632 let instrument_id = instrument.id();
633 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
634
635 FundingRateUpdate::new(
636 instrument_id,
637 msg.interest,
638 None, None, ts_event,
641 ts_init,
642 )
643}
644
645pub fn resolution_to_bar_type(
653 instrument_id: InstrumentId,
654 resolution: &str,
655) -> anyhow::Result<BarType> {
656 let (step, aggregation) = match resolution {
657 "1" => (1, BarAggregation::Minute),
658 "3" => (3, BarAggregation::Minute),
659 "5" => (5, BarAggregation::Minute),
660 "10" => (10, BarAggregation::Minute),
661 "15" => (15, BarAggregation::Minute),
662 "30" => (30, BarAggregation::Minute),
663 "60" => (1, BarAggregation::Hour),
664 "120" => (2, BarAggregation::Hour),
665 "180" => (3, BarAggregation::Hour),
666 "360" => (6, BarAggregation::Hour),
667 "720" => (12, BarAggregation::Hour),
668 "1D" => (1, BarAggregation::Day),
669 _ => anyhow::bail!("Unsupported Deribit resolution: {resolution}"),
670 };
671
672 let spec = BarSpecification::new_checked(step, aggregation, PriceType::Last)
673 .context("invalid Deribit bar resolution")?;
674 Ok(BarType::new(
675 instrument_id,
676 spec,
677 AggregationSource::External,
678 ))
679}
680
681pub fn parse_chart_msg(
697 chart_msg: &DeribitChartMsg,
698 bar_type: BarType,
699 price_precision: u8,
700 size_precision: u8,
701 use_cost_for_volume: bool,
702 timestamp_on_close: bool,
703 ts_init: UnixNanos,
704) -> anyhow::Result<Bar> {
705 let open =
706 Price::from_decimal_dp(chart_msg.open, price_precision).context("Invalid open price")?;
707 let high =
708 Price::from_decimal_dp(chart_msg.high, price_precision).context("Invalid high price")?;
709 let low =
710 Price::from_decimal_dp(chart_msg.low, price_precision).context("Invalid low price")?;
711 let close =
712 Price::from_decimal_dp(chart_msg.close, price_precision).context("Invalid close price")?;
713
714 let raw_volume = if use_cost_for_volume {
715 chart_msg.cost
716 } else {
717 chart_msg.volume
718 };
719
720 let volume = Quantity::from_decimal_dp(raw_volume, size_precision).context("Invalid volume")?;
721
722 let mut ts_event = UnixNanos::from(chart_msg.tick * NANOSECONDS_IN_MILLISECOND);
724
725 if timestamp_on_close {
727 let interval_ns = bar_type.spec().timedelta().as_nanos();
728 let interval_ns = u64::try_from(interval_ns)
729 .context("bar interval overflowed the u64 range for nanoseconds")?;
730 let updated = ts_event
731 .as_u64()
732 .checked_add(interval_ns)
733 .context("bar timestamp overflowed when adjusting to close time")?;
734 ts_event = UnixNanos::from(updated);
735 }
736
737 Bar::new_checked(bar_type, open, high, low, close, volume, ts_event, ts_init)
738 .context("Invalid OHLC bar")
739}
740
741pub fn parse_user_order_msg(
747 msg: &DeribitOrderMsg,
748 instrument: &InstrumentAny,
749 account_id: AccountId,
750 ts_init: UnixNanos,
751) -> anyhow::Result<OrderStatusReport> {
752 let instrument_id = instrument.id();
753 let venue_order_id = VenueOrderId::new(&msg.order_id);
754
755 let order_side = match msg.direction.as_str() {
756 "buy" => OrderSide::Buy,
757 "sell" => OrderSide::Sell,
758 _ => anyhow::bail!("Unknown order direction: {}", msg.direction),
759 };
760
761 let order_type = parse_deribit_order_type(&msg.order_type);
763
764 let time_in_force = match msg.time_in_force.as_str() {
766 "good_til_cancelled" => TimeInForce::Gtc,
767 "good_til_day" => TimeInForce::Gtd,
768 "fill_or_kill" => TimeInForce::Fok,
769 "immediate_or_cancel" => TimeInForce::Ioc,
770 other => {
771 log::warn!("Unknown time_in_force '{other}', defaulting to GTC");
772 TimeInForce::Gtc
773 }
774 };
775
776 let order_status = match msg.order_state.as_str() {
778 "open" => {
779 if msg.filled_amount.is_zero() {
780 OrderStatus::Accepted
781 } else {
782 OrderStatus::PartiallyFilled
783 }
784 }
785 "filled" => OrderStatus::Filled,
786 "rejected" => OrderStatus::Rejected,
787 "cancelled" => OrderStatus::Canceled,
788 "untriggered" => OrderStatus::Accepted, other => {
790 log::warn!("Unknown Deribit order_state '{other}', defaulting to Accepted");
791 OrderStatus::Accepted
792 }
793 };
794
795 let price_precision = instrument.price_precision();
796 let size_precision = instrument.size_precision();
797
798 let quantity = Quantity::from_decimal_dp(msg.amount, size_precision)?;
799 let filled_qty = Quantity::from_decimal_dp(msg.filled_amount, size_precision)?;
800
801 let ts_accepted = UnixNanos::new(msg.creation_timestamp * NANOSECONDS_IN_MILLISECOND);
802 let ts_last = UnixNanos::new(msg.last_update_timestamp * NANOSECONDS_IN_MILLISECOND);
803
804 let mut report = OrderStatusReport::new(
805 account_id,
806 instrument_id,
807 None, venue_order_id,
809 order_side.into(),
810 order_type,
811 time_in_force,
812 order_status,
813 quantity,
814 filled_qty,
815 ts_accepted,
816 ts_last,
817 ts_init,
818 Some(UUID4::new()),
819 );
820
821 if let Some(ref label) = msg.label
823 && !label.is_empty()
824 {
825 report = report.with_client_order_id(ClientOrderId::new(label));
826 }
827
828 if let Some(price_val) = msg.price
830 && !price_val.is_zero()
831 {
832 let price = Price::from_decimal_dp(price_val, price_precision)?;
833 report = report.with_price(price);
834 }
835
836 if time_in_force == TimeInForce::Gtd {
837 let expire_time = next_8_utc(ts_accepted)?;
838 report = report.with_expire_time(expire_time);
839 }
840
841 if let Some(avg_price) = msg.average_price
843 && !avg_price.is_zero()
844 {
845 report = report.with_avg_px(avg_price);
846 }
847
848 if let Some(trigger_price) = msg.trigger_price
850 && !trigger_price.is_zero()
851 {
852 let trigger = Price::from_decimal_dp(trigger_price, price_precision)?;
853 report = report.with_trigger_price(trigger);
854 }
855
856 if msg.post_only {
857 report = report.with_post_only(true);
858 }
859
860 if msg.reduce_only {
861 report = report.with_reduce_only(true);
862 }
863
864 if let Some(ref reason) = msg.reject_reason {
866 report = report.with_cancel_reason(reason.clone());
867 } else if let Some(ref reason) = msg.cancel_reason {
868 report = report.with_cancel_reason(reason.clone());
869 }
870
871 Ok(report)
872}
873
874#[must_use]
875pub(crate) fn parse_deribit_order_type(order_type: &str) -> OrderType {
876 match order_type {
877 "limit" => OrderType::Limit,
878 "market" => OrderType::Market,
879 "stop_limit" => OrderType::StopLimit,
880 "stop_market" => OrderType::StopMarket,
881 "take_limit" => OrderType::LimitIfTouched,
882 "take_market" => OrderType::MarketIfTouched,
883 other => {
884 log::warn!("Unknown Deribit order_type '{other}', defaulting to Limit");
885 OrderType::Limit
886 }
887 }
888}
889
890pub fn parse_user_trade_msg(
896 msg: &DeribitUserTradeMsg,
897 instrument: &InstrumentAny,
898 account_id: AccountId,
899 ts_init: UnixNanos,
900) -> anyhow::Result<FillReport> {
901 let instrument_id = instrument.id();
902 let venue_order_id = VenueOrderId::new(&msg.order_id);
903 let trade_id = TradeId::new(&msg.trade_id);
904
905 if let Some(liq) = msg.liquidation.as_deref().filter(|s| !s.is_empty()) {
908 let who = match liq {
909 "M" => "maker",
910 "T" => "taker",
911 "MT" => "both",
912 _ => liq,
913 };
914 log::warn!(
915 "Liquidation trade: {} trade_id={} order_id={} liquidation_side={} direction={} amount={} price={}",
916 instrument_id,
917 msg.trade_id,
918 msg.order_id,
919 who,
920 msg.direction,
921 msg.amount,
922 msg.price,
923 );
924 }
925
926 let order_side = match msg.direction.as_str() {
927 "buy" => OrderSide::Buy,
928 "sell" => OrderSide::Sell,
929 _ => anyhow::bail!("Unknown trade direction: {}", msg.direction),
930 };
931
932 let price_precision = instrument.price_precision();
933 let size_precision = instrument.size_precision();
934
935 let last_qty = Quantity::from_decimal_dp(msg.amount, size_precision)?;
936 let last_px = Price::from_decimal_dp(msg.price, price_precision)?;
937
938 let liquidity_side = match msg.liquidity.as_str() {
939 "M" => LiquiditySide::Maker,
940 "T" => LiquiditySide::Taker,
941 _ => LiquiditySide::NoLiquiditySide,
942 };
943
944 let fee_currency = Currency::from(&msg.fee_currency);
946 let commission = Money::from_decimal(msg.fee, fee_currency)?;
947
948 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
949
950 let client_order_id = msg
951 .label
952 .as_ref()
953 .filter(|l| !l.is_empty())
954 .map(ClientOrderId::new);
955
956 Ok(FillReport::new(
957 account_id,
958 instrument_id,
959 venue_order_id,
960 trade_id,
961 order_side,
962 last_qty,
963 last_px,
964 commission,
965 liquidity_side,
966 client_order_id,
967 None, ts_event,
969 ts_init,
970 None, ))
972}
973
974#[must_use]
985pub fn parse_position_status_report(
986 position: &DeribitPosition,
987 instrument: &InstrumentAny,
988 account_id: AccountId,
989 ts_init: UnixNanos,
990) -> PositionStatusReport {
991 let instrument_id = instrument.id();
992 let size_precision = instrument.size_precision();
993
994 let signed_qty = Quantity::from_decimal_dp(position.size.abs(), size_precision)
995 .unwrap_or_else(|_| Quantity::zero(size_precision));
996
997 let position_side = match position.direction.as_str() {
998 "buy" => PositionSide::Long,
999 "sell" => PositionSide::Short,
1000 _ => PositionSide::Flat,
1001 };
1002
1003 let avg_px_open = Some(position.average_price);
1005
1006 PositionStatusReport::new(
1007 account_id,
1008 instrument_id,
1009 position_side,
1010 signed_qty,
1011 ts_init,
1012 ts_init,
1013 Some(UUID4::new()),
1014 None, avg_px_open,
1016 )
1017}
1018
1019#[derive(Debug, Clone)]
1024pub enum ParsedOrderEvent {
1025 Accepted(OrderAccepted),
1027 Canceled(OrderCanceled),
1029 Expired(OrderExpired),
1031 Updated(OrderUpdated),
1033 None,
1035}
1036
1037fn extract_client_order_id(msg: &DeribitOrderMsg) -> Option<ClientOrderId> {
1039 msg.label
1040 .as_ref()
1041 .filter(|l| !l.is_empty())
1042 .map(ClientOrderId::new)
1043}
1044
1045#[must_use]
1050pub fn parse_order_accepted(
1051 msg: &DeribitOrderMsg,
1052 instrument: &InstrumentAny,
1053 account_id: AccountId,
1054 trader_id: TraderId,
1055 strategy_id: StrategyId,
1056 ts_init: UnixNanos,
1057) -> OrderAccepted {
1058 let client_order_id =
1059 extract_client_order_id(msg).unwrap_or_else(|| ClientOrderId::new(&msg.order_id));
1060 parse_order_accepted_with_client_order_id(
1061 msg,
1062 instrument,
1063 account_id,
1064 trader_id,
1065 strategy_id,
1066 client_order_id,
1067 ts_init,
1068 )
1069}
1070
1071#[must_use]
1072pub(crate) fn parse_order_accepted_with_client_order_id(
1073 msg: &DeribitOrderMsg,
1074 instrument: &InstrumentAny,
1075 account_id: AccountId,
1076 trader_id: TraderId,
1077 strategy_id: StrategyId,
1078 client_order_id: ClientOrderId,
1079 ts_init: UnixNanos,
1080) -> OrderAccepted {
1081 let instrument_id = instrument.id();
1082 let venue_order_id = VenueOrderId::new(&msg.order_id);
1083 let ts_event = UnixNanos::new(msg.last_update_timestamp * NANOSECONDS_IN_MILLISECOND);
1084
1085 OrderAccepted::new(
1086 trader_id,
1087 strategy_id,
1088 instrument_id,
1089 client_order_id,
1090 venue_order_id,
1091 account_id,
1092 nautilus_core::UUID4::new(),
1093 ts_event,
1094 ts_init,
1095 false, )
1097}
1098
1099#[must_use]
1103pub fn parse_order_canceled(
1104 msg: &DeribitOrderMsg,
1105 instrument: &InstrumentAny,
1106 account_id: AccountId,
1107 trader_id: TraderId,
1108 strategy_id: StrategyId,
1109 ts_init: UnixNanos,
1110) -> OrderCanceled {
1111 let client_order_id =
1112 extract_client_order_id(msg).unwrap_or_else(|| ClientOrderId::new(&msg.order_id));
1113 parse_order_canceled_with_client_order_id(
1114 msg,
1115 instrument,
1116 account_id,
1117 trader_id,
1118 strategy_id,
1119 client_order_id,
1120 ts_init,
1121 )
1122}
1123
1124#[must_use]
1125pub(crate) fn parse_order_canceled_with_client_order_id(
1126 msg: &DeribitOrderMsg,
1127 instrument: &InstrumentAny,
1128 account_id: AccountId,
1129 trader_id: TraderId,
1130 strategy_id: StrategyId,
1131 client_order_id: ClientOrderId,
1132 ts_init: UnixNanos,
1133) -> OrderCanceled {
1134 let instrument_id = instrument.id();
1135 let venue_order_id = VenueOrderId::new(&msg.order_id);
1136 let ts_event = UnixNanos::new(msg.last_update_timestamp * NANOSECONDS_IN_MILLISECOND);
1137
1138 OrderCanceled::new(
1139 trader_id,
1140 strategy_id,
1141 instrument_id,
1142 client_order_id,
1143 nautilus_core::UUID4::new(),
1144 ts_event,
1145 ts_init,
1146 false, Some(venue_order_id),
1148 Some(account_id),
1149 msg.cancel_reason.as_deref().map(Ustr::from),
1150 )
1151}
1152
1153#[must_use]
1158pub fn parse_order_expired(
1159 msg: &DeribitOrderMsg,
1160 instrument: &InstrumentAny,
1161 account_id: AccountId,
1162 trader_id: TraderId,
1163 strategy_id: StrategyId,
1164 ts_init: UnixNanos,
1165) -> OrderExpired {
1166 let client_order_id =
1167 extract_client_order_id(msg).unwrap_or_else(|| ClientOrderId::new(&msg.order_id));
1168 parse_order_expired_with_client_order_id(
1169 msg,
1170 instrument,
1171 account_id,
1172 trader_id,
1173 strategy_id,
1174 client_order_id,
1175 ts_init,
1176 )
1177}
1178
1179#[must_use]
1180pub(crate) fn parse_order_expired_with_client_order_id(
1181 msg: &DeribitOrderMsg,
1182 instrument: &InstrumentAny,
1183 account_id: AccountId,
1184 trader_id: TraderId,
1185 strategy_id: StrategyId,
1186 client_order_id: ClientOrderId,
1187 ts_init: UnixNanos,
1188) -> OrderExpired {
1189 let instrument_id = instrument.id();
1190 let venue_order_id = VenueOrderId::new(&msg.order_id);
1191 let ts_event = UnixNanos::new(msg.last_update_timestamp * NANOSECONDS_IN_MILLISECOND);
1192
1193 OrderExpired::new(
1194 trader_id,
1195 strategy_id,
1196 instrument_id,
1197 client_order_id,
1198 nautilus_core::UUID4::new(),
1199 ts_event,
1200 ts_init,
1201 false, Some(venue_order_id),
1203 Some(account_id),
1204 )
1205}
1206
1207#[must_use]
1211pub fn parse_order_updated(
1212 msg: &DeribitOrderMsg,
1213 instrument: &InstrumentAny,
1214 account_id: AccountId,
1215 trader_id: TraderId,
1216 strategy_id: StrategyId,
1217 ts_init: UnixNanos,
1218) -> OrderUpdated {
1219 let client_order_id =
1220 extract_client_order_id(msg).unwrap_or_else(|| ClientOrderId::new(&msg.order_id));
1221 parse_order_updated_with_client_order_id(
1222 msg,
1223 instrument,
1224 account_id,
1225 trader_id,
1226 strategy_id,
1227 client_order_id,
1228 ts_init,
1229 )
1230}
1231
1232#[must_use]
1233pub(crate) fn parse_order_updated_with_client_order_id(
1234 msg: &DeribitOrderMsg,
1235 instrument: &InstrumentAny,
1236 account_id: AccountId,
1237 trader_id: TraderId,
1238 strategy_id: StrategyId,
1239 client_order_id: ClientOrderId,
1240 ts_init: UnixNanos,
1241) -> OrderUpdated {
1242 let instrument_id = instrument.id();
1243 let price_precision = instrument.price_precision();
1244 let size_precision = instrument.size_precision();
1245
1246 let venue_order_id = VenueOrderId::new(&msg.order_id);
1247 let quantity = Quantity::from_decimal_dp(msg.amount, size_precision)
1248 .unwrap_or_else(|_| Quantity::zero(size_precision));
1249 let price = msg
1250 .price
1251 .and_then(|p| Price::from_decimal_dp(p, price_precision).ok());
1252 let trigger_price = msg
1253 .trigger_price
1254 .and_then(|p| Price::from_decimal_dp(p, price_precision).ok());
1255 let ts_event = UnixNanos::new(msg.last_update_timestamp * NANOSECONDS_IN_MILLISECOND);
1256
1257 OrderUpdated::new(
1258 trader_id,
1259 strategy_id,
1260 instrument_id,
1261 client_order_id,
1262 quantity,
1263 nautilus_core::UUID4::new(),
1264 ts_event,
1265 ts_init,
1266 false, Some(venue_order_id),
1268 Some(account_id),
1269 price,
1270 trigger_price,
1271 None, false, )
1274}
1275
1276#[must_use]
1289pub fn determine_order_event_type(
1290 order_state: &str,
1291 is_new_order: bool,
1292 was_amended: bool,
1293) -> OrderEventType {
1294 match order_state {
1295 "open" | "untriggered" => {
1296 if was_amended {
1297 OrderEventType::Updated
1298 } else if is_new_order {
1299 OrderEventType::Accepted
1300 } else {
1301 OrderEventType::None
1303 }
1304 }
1305 "cancelled" => OrderEventType::Canceled,
1306 "expired" => OrderEventType::Expired,
1307 "filled" => {
1308 OrderEventType::None
1310 }
1311 "rejected" => {
1312 OrderEventType::None
1314 }
1315 other => {
1316 log::warn!("Unknown Deribit order_state '{other}' in event routing, dropping");
1317 OrderEventType::None
1318 }
1319 }
1320}
1321
1322#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1324pub enum OrderEventType {
1325 Accepted,
1327 Canceled,
1329 Expired,
1331 Updated,
1333 None,
1335}
1336
1337#[cfg(test)]
1338mod tests {
1339 use rstest::rstest;
1340 use rust_decimal_macros::dec;
1341 use serde::de::DeserializeOwned;
1342
1343 use super::*;
1344 use crate::{
1345 common::{
1346 parse::{parse_deribit_instrument_any, parse_portfolio_to_account_state},
1347 testing::load_test_json,
1348 },
1349 http::models::{DeribitInstrument, DeribitJsonRpcResponse},
1350 websocket::messages::{DeribitPortfolioMsg, DeribitWsMessage, parse_raw_message},
1351 };
1352
1353 fn test_perpetual_instrument() -> InstrumentAny {
1355 let json = load_test_json("http_get_instruments.json");
1356 let response: DeribitJsonRpcResponse<Vec<DeribitInstrument>> =
1357 serde_json::from_str(&json).unwrap();
1358 let instrument = &response.result.unwrap()[0];
1359 parse_deribit_instrument_any(instrument, UnixNanos::default(), UnixNanos::default())
1360 .unwrap()
1361 .unwrap()
1362 }
1363
1364 #[rstest]
1365 fn test_parse_trade_msg_sell() {
1366 let instrument = test_perpetual_instrument();
1367 let json = load_test_json("ws_trades.json");
1368 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1369 let trades: Vec<DeribitTradeMsg> =
1370 serde_json::from_str(&response["params"]["data"].to_string()).unwrap();
1371 let msg = &trades[0];
1372
1373 let tick = parse_trade_msg(msg, &instrument, UnixNanos::default()).unwrap();
1374
1375 assert_eq!(tick.instrument_id, instrument.id());
1376 assert_eq!(tick.price, instrument.make_price(92294.5));
1377 assert_eq!(tick.size, instrument.make_qty(10.0, None));
1378 assert_eq!(tick.aggressor_side, AggressorSide::Sell);
1379 assert_eq!(tick.trade_id.to_string(), "403691824");
1380 assert_eq!(tick.ts_event, UnixNanos::new(1_765_531_356_452_000_000));
1381 }
1382
1383 #[rstest]
1384 fn test_parse_trade_msg_buy() {
1385 let instrument = test_perpetual_instrument();
1386 let json = load_test_json("ws_trades.json");
1387 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1388 let trades: Vec<DeribitTradeMsg> =
1389 serde_json::from_str(&response["params"]["data"].to_string()).unwrap();
1390 let msg = &trades[1];
1391
1392 let tick = parse_trade_msg(msg, &instrument, UnixNanos::default()).unwrap();
1393
1394 assert_eq!(tick.instrument_id, instrument.id());
1395 assert_eq!(tick.price, instrument.make_price(92288.5));
1396 assert_eq!(tick.size, instrument.make_qty(750.0, None));
1397 assert_eq!(tick.aggressor_side, AggressorSide::Sell);
1398 assert_eq!(tick.trade_id.to_string(), "403691825");
1399 }
1400
1401 fn make_trade_msg(
1402 instrument_name: &str,
1403 trade_id: &str,
1404 block_trade_id: Option<&str>,
1405 block_rfq_id: Option<i64>,
1406 combo_id: Option<&str>,
1407 ) -> DeribitTradeMsg {
1408 let raw = serde_json::json!({
1409 "trade_id": trade_id,
1410 "instrument_name": instrument_name,
1411 "price": 92294.5,
1412 "amount": 10.0,
1413 "direction": "buy",
1414 "timestamp": 1_765_531_356_452_u64,
1415 "trade_seq": 1,
1416 "tick_direction": 0,
1417 "index_price": 92276.75,
1418 "mark_price": 92287.11,
1419 "block_trade_id": block_trade_id,
1420 "block_rfq_id": block_rfq_id,
1421 "combo_id": combo_id,
1422 });
1423 serde_json::from_str(&raw.to_string()).unwrap()
1424 }
1425
1426 #[rstest]
1427 fn test_parse_trade_msg_tags_block_trade() {
1428 let instrument = test_perpetual_instrument();
1429 let msg = make_trade_msg("BTC-PERPETUAL", "244343055", Some("12345"), None, None);
1430 let tick = parse_trade_msg(&msg, &instrument, UnixNanos::default()).unwrap();
1431 assert_eq!(tick.trade_id.to_string(), "BLK-244343055");
1432 }
1433
1434 #[rstest]
1435 fn test_parse_trade_msg_tags_block_rfq() {
1436 let instrument = test_perpetual_instrument();
1437 let msg = make_trade_msg("BTC-PERPETUAL", "244343055", None, Some(99), None);
1438 let tick = parse_trade_msg(&msg, &instrument, UnixNanos::default()).unwrap();
1439 assert_eq!(tick.trade_id.to_string(), "RFQ-244343055");
1440 }
1441
1442 #[rstest]
1443 fn test_parse_trade_msg_tags_combo_leg() {
1444 let instrument = test_perpetual_instrument();
1448 let msg = make_trade_msg(
1449 "BTC-PERPETUAL",
1450 "244343055",
1451 None,
1452 None,
1453 Some("BTC-FS-25DEC26_PERP"),
1454 );
1455 let tick = parse_trade_msg(&msg, &instrument, UnixNanos::default()).unwrap();
1456 assert_eq!(tick.trade_id.to_string(), "COMBO-244343055");
1457 }
1458
1459 fn load_combo_option_instrument() -> InstrumentAny {
1460 let combo_json = load_test_json("http_get_instruments_option_combo.json");
1461 let combo_response: DeribitJsonRpcResponse<Vec<DeribitInstrument>> =
1462 serde_json::from_str(&combo_json).unwrap();
1463 let combo_raw = combo_response
1464 .result
1465 .unwrap()
1466 .into_iter()
1467 .find(|i| i.instrument_name == "BTC-CS-19MAY26-70000_75000")
1468 .expect("fixture must contain BTC-CS-19MAY26-70000_75000");
1469 parse_deribit_instrument_any(&combo_raw, UnixNanos::default(), UnixNanos::default())
1470 .unwrap()
1471 .unwrap()
1472 }
1473
1474 fn load_combo_trade_msgs() -> Vec<DeribitTradeMsg> {
1475 let json = load_test_json("ws_trades_option_combo.json");
1476 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1477 serde_json::from_str(&response["params"]["data"].to_string()).unwrap()
1478 }
1479
1480 #[rstest]
1481 fn test_parse_trades_data_combo_emits_single_tick() {
1482 let combo_inst = load_combo_option_instrument();
1486 let mut cache: AHashMap<Ustr, InstrumentAny> = AHashMap::new();
1487 cache.insert(Ustr::from("BTC-CS-19MAY26-70000_75000"), combo_inst.clone());
1488
1489 let trades = load_combo_trade_msgs();
1490 let legs = trades[0].legs.as_ref().expect("combo trade must have legs");
1492 assert_eq!(legs.len(), 2);
1493
1494 let data = parse_trades_data(&trades, &cache, UnixNanos::default());
1495 assert_eq!(data.len(), 1, "should emit one tick for the combo only");
1496
1497 let Data::Trade(tick) = &data[0] else {
1498 panic!("expected Data::Trade");
1499 };
1500 assert_eq!(tick.instrument_id, combo_inst.id());
1501 assert_eq!(tick.price, Price::from("0.0639"));
1504 assert_eq!(tick.size, Quantity::from("0.1"));
1505 assert_eq!(tick.aggressor_side, AggressorSide::Sell);
1506 assert_eq!(tick.trade_id.to_string(), "244365193");
1507 }
1508
1509 #[rstest]
1510 fn test_parse_trades_data_combo_not_cached_emits_no_tick() {
1511 let cache: AHashMap<Ustr, InstrumentAny> = AHashMap::new();
1515 let trades = load_combo_trade_msgs();
1516 let data = parse_trades_data(&trades, &cache, UnixNanos::default());
1517 assert!(data.is_empty(), "uncached combo must not emit ticks");
1518 }
1519
1520 #[rstest]
1521 fn test_parse_trades_data_mixed_combo_and_per_leg() {
1522 let combo_inst = load_combo_option_instrument();
1525 let perp_inst = test_perpetual_instrument();
1526
1527 let mut cache: AHashMap<Ustr, InstrumentAny> = AHashMap::new();
1528 cache.insert(Ustr::from("BTC-CS-19MAY26-70000_75000"), combo_inst.clone());
1529 cache.insert(Ustr::from("BTC-PERPETUAL"), perp_inst.clone());
1530
1531 let mut trades = load_combo_trade_msgs();
1532 let perp_msgs: Vec<DeribitTradeMsg> = {
1533 let json = load_test_json("ws_trades.json");
1534 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1535 serde_json::from_str(&response["params"]["data"].to_string()).unwrap()
1536 };
1537 trades.extend(perp_msgs);
1538
1539 let data = parse_trades_data(&trades, &cache, UnixNanos::default());
1540 assert_eq!(data.len(), 3);
1542
1543 let mut combo_ticks = 0;
1544 let mut perp_ticks = 0;
1545
1546 for item in &data {
1547 let Data::Trade(tick) = item else {
1548 panic!("expected Data::Trade, was {item:?}");
1549 };
1550
1551 if tick.instrument_id == combo_inst.id() {
1552 combo_ticks += 1;
1553 assert_eq!(tick.trade_id.to_string(), "244365193");
1554 } else if tick.instrument_id == perp_inst.id() {
1555 perp_ticks += 1;
1556 } else {
1557 panic!("unexpected instrument_id: {}", tick.instrument_id);
1558 }
1559 }
1560 assert_eq!(combo_ticks, 1);
1561 assert_eq!(perp_ticks, 2);
1562 }
1563
1564 #[rstest]
1565 fn test_parse_book_snapshot() {
1566 let instrument = test_perpetual_instrument();
1567 let json = load_test_json("ws_book_snapshot.json");
1568 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1569 let data = response["params"]["data"].to_string();
1570 let msg: DeribitBookMsg = serde_json::from_str(&data).unwrap();
1571
1572 let deltas = parse_book_snapshot(&msg, &instrument, UnixNanos::default()).unwrap();
1573
1574 assert_eq!(deltas.instrument_id, instrument.id());
1575 assert_eq!(deltas.deltas.len(), 11);
1577
1578 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
1580
1581 let first_bid = &deltas.deltas[1];
1583 assert_eq!(first_bid.action, BookAction::Add);
1584 assert_eq!(first_bid.order.side, OrderSide::Buy.into());
1585 assert_eq!(first_bid.order.price, instrument.make_price(42500.0));
1586 assert_eq!(first_bid.order.size, instrument.make_qty(1000.0, None));
1587
1588 let first_ask = &deltas.deltas[6];
1590 assert_eq!(first_ask.action, BookAction::Add);
1591 assert_eq!(first_ask.order.side, OrderSide::Sell.into());
1592 assert_eq!(first_ask.order.price, instrument.make_price(42501.0));
1593 assert_eq!(first_ask.order.size, instrument.make_qty(800.0, None));
1594
1595 let last = deltas.deltas.last().unwrap();
1597 assert_eq!(
1598 last.flags & RecordFlag::F_LAST as u8,
1599 RecordFlag::F_LAST as u8
1600 );
1601 }
1602
1603 #[rstest]
1604 fn test_parse_book_delta() {
1605 let instrument = test_perpetual_instrument();
1606 let json = load_test_json("ws_book_delta.json");
1607 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1608 let data = response["params"]["data"].to_string();
1609 let msg: DeribitBookMsg = serde_json::from_str(&data).unwrap();
1610
1611 let deltas = parse_book_delta(&msg, &instrument, UnixNanos::default()).unwrap();
1612
1613 assert_eq!(deltas.instrument_id, instrument.id());
1614 assert_eq!(deltas.deltas.len(), 4);
1616
1617 let bid_change = &deltas.deltas[0];
1619 assert_eq!(bid_change.action, BookAction::Update);
1620 assert_eq!(bid_change.order.side, OrderSide::Buy.into());
1621 assert_eq!(bid_change.order.price, instrument.make_price(42500.0));
1622 assert_eq!(bid_change.order.size, instrument.make_qty(950.0, None));
1623
1624 let bid_new = &deltas.deltas[1];
1626 assert_eq!(bid_new.action, BookAction::Add);
1627 assert_eq!(bid_new.order.side, OrderSide::Buy.into());
1628 assert_eq!(bid_new.order.price, instrument.make_price(42498.5));
1629 assert_eq!(bid_new.order.size, instrument.make_qty(300.0, None));
1630
1631 let ask_delete = &deltas.deltas[2];
1633 assert_eq!(ask_delete.action, BookAction::Delete);
1634 assert_eq!(ask_delete.order.side, OrderSide::Sell.into());
1635 assert_eq!(ask_delete.order.price, instrument.make_price(42501.0));
1636 assert_eq!(ask_delete.order.size, instrument.make_qty(0.0, None));
1637
1638 let ask_change = &deltas.deltas[3];
1640 assert_eq!(ask_change.action, BookAction::Update);
1641 assert_eq!(ask_change.order.side, OrderSide::Sell.into());
1642 assert_eq!(ask_change.order.price, instrument.make_price(42501.5));
1643 assert_eq!(ask_change.order.size, instrument.make_qty(700.0, None));
1644
1645 let last = deltas.deltas.last().unwrap();
1647 assert_eq!(
1648 last.flags & RecordFlag::F_LAST as u8,
1649 RecordFlag::F_LAST as u8
1650 );
1651 }
1652
1653 #[rstest]
1654 fn test_parse_ticker_to_quote() {
1655 let instrument = test_perpetual_instrument();
1656 let json = load_test_json("ws_ticker.json");
1657 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1658 let msg: DeribitTickerMsg =
1659 serde_json::from_str(&response["params"]["data"].to_string()).unwrap();
1660
1661 assert_eq!(msg.instrument_name, "BTC-PERPETUAL");
1663 assert_eq!(msg.timestamp, 1_765_541_474_086);
1664 assert_eq!(msg.best_bid_price, Some(dec!(92283.5)));
1665 assert_eq!(msg.best_ask_price, Some(dec!(92284.0)));
1666 assert_eq!(msg.best_bid_amount, Some(dec!(117660.0)));
1667 assert_eq!(msg.best_ask_amount, Some(dec!(186520.0)));
1668 assert_eq!(msg.mark_price, dec!(92281.78));
1669 assert_eq!(msg.index_price, dec!(92263.55));
1670 assert_eq!(msg.open_interest, dec!(1132329370.0));
1671
1672 let quote = parse_ticker_to_quote(&msg, &instrument, UnixNanos::default()).unwrap();
1673
1674 assert_eq!(quote.instrument_id, instrument.id());
1675 assert_eq!(quote.bid_price, instrument.make_price(92283.5));
1676 assert_eq!(quote.ask_price, instrument.make_price(92284.0));
1677 assert_eq!(quote.bid_size, instrument.make_qty(117660.0, None));
1678 assert_eq!(quote.ask_size, instrument.make_qty(186520.0, None));
1679 assert_eq!(quote.ts_event, UnixNanos::new(1_765_541_474_086_000_000));
1680 }
1681
1682 #[rstest]
1683 fn test_parse_quote_msg() {
1684 let instrument = test_perpetual_instrument();
1685 let json = load_test_json("ws_quote.json");
1686 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1687 let msg: DeribitQuoteMsg =
1688 serde_json::from_str(&response["params"]["data"].to_string()).unwrap();
1689
1690 assert_eq!(msg.instrument_name, "BTC-PERPETUAL");
1692 assert_eq!(msg.timestamp, 1_765_541_767_174);
1693 assert_eq!(msg.best_bid_price, dec!(92288.0));
1694 assert_eq!(msg.best_ask_price, dec!(92288.5));
1695 assert_eq!(msg.best_bid_amount, dec!(133440.0));
1696 assert_eq!(msg.best_ask_amount, dec!(99470.0));
1697
1698 let quote = parse_quote_msg(&msg, &instrument, UnixNanos::default()).unwrap();
1699
1700 assert_eq!(quote.instrument_id, instrument.id());
1701 assert_eq!(quote.bid_price, instrument.make_price(92288.0));
1702 assert_eq!(quote.ask_price, instrument.make_price(92288.5));
1703 assert_eq!(quote.bid_size, instrument.make_qty(133440.0, None));
1704 assert_eq!(quote.ask_size, instrument.make_qty(99470.0, None));
1705 assert_eq!(quote.ts_event, UnixNanos::new(1_765_541_767_174_000_000));
1706 }
1707
1708 #[rstest]
1709 fn test_parse_book_msg_snapshot() {
1710 let instrument = test_perpetual_instrument();
1711 let json = load_test_json("ws_book_snapshot.json");
1712 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1713 let data = response["params"]["data"].to_string();
1714 let msg: DeribitBookMsg = serde_json::from_str(&data).unwrap();
1715
1716 assert_eq!(
1718 msg.bids[0].len(),
1719 3,
1720 "Snapshot bids should have 3 elements: [action, price, amount]"
1721 );
1722 assert_eq!(
1723 msg.bids[0][0].get(),
1724 r#""new""#,
1725 "First element should be 'new' action for snapshot"
1726 );
1727 assert_eq!(
1728 msg.asks[0].len(),
1729 3,
1730 "Snapshot asks should have 3 elements: [action, price, amount]"
1731 );
1732 assert_eq!(
1733 msg.asks[0][0].get(),
1734 r#""new""#,
1735 "First element should be 'new' action for snapshot"
1736 );
1737
1738 let deltas = parse_book_msg(&msg, &instrument, UnixNanos::default()).unwrap();
1739
1740 assert_eq!(deltas.instrument_id, instrument.id());
1741 assert_eq!(deltas.deltas.len(), 11);
1743
1744 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
1746
1747 let first_bid = &deltas.deltas[1];
1749 assert_eq!(first_bid.action, BookAction::Add);
1750 assert_eq!(first_bid.order.side, OrderSide::Buy.into());
1751 assert_eq!(first_bid.order.price, instrument.make_price(42500.0));
1752 assert_eq!(first_bid.order.size, instrument.make_qty(1000.0, None));
1753
1754 let first_ask = &deltas.deltas[6];
1756 assert_eq!(first_ask.action, BookAction::Add);
1757 assert_eq!(first_ask.order.side, OrderSide::Sell.into());
1758 assert_eq!(first_ask.order.price, instrument.make_price(42501.0));
1759 assert_eq!(first_ask.order.size, instrument.make_qty(800.0, None));
1760 }
1761
1762 #[rstest]
1763 fn test_parse_book_msg_delta() {
1764 let instrument = test_perpetual_instrument();
1765 let json = load_test_json("ws_book_delta.json");
1766 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1767 let data = response["params"]["data"].to_string();
1768 let msg: DeribitBookMsg = serde_json::from_str(&data).unwrap();
1769
1770 assert_eq!(
1772 msg.bids[0].len(),
1773 3,
1774 "Delta bids should have 3 elements: [action, price, amount]"
1775 );
1776 assert_eq!(
1777 msg.bids[0][0].get(),
1778 r#""change""#,
1779 "First bid should be 'change' action"
1780 );
1781 assert_eq!(
1782 msg.bids[1][0].get(),
1783 r#""new""#,
1784 "Second bid should be 'new' action"
1785 );
1786 assert_eq!(
1787 msg.asks[0].len(),
1788 3,
1789 "Delta asks should have 3 elements: [action, price, amount]"
1790 );
1791 assert_eq!(
1792 msg.asks[0][0].get(),
1793 r#""delete""#,
1794 "First ask should be 'delete' action"
1795 );
1796
1797 let deltas = parse_book_msg(&msg, &instrument, UnixNanos::default()).unwrap();
1798
1799 assert_eq!(deltas.instrument_id, instrument.id());
1800 assert_eq!(deltas.deltas.len(), 4);
1802
1803 assert_ne!(deltas.deltas[0].action, BookAction::Clear);
1805
1806 let bid_change = &deltas.deltas[0];
1808 assert_eq!(bid_change.action, BookAction::Update);
1809 assert_eq!(bid_change.order.side, OrderSide::Buy.into());
1810 assert_eq!(bid_change.order.price, instrument.make_price(42500.0));
1811 assert_eq!(bid_change.order.size, instrument.make_qty(950.0, None));
1812
1813 let bid_new = &deltas.deltas[1];
1815 assert_eq!(bid_new.action, BookAction::Add);
1816 assert_eq!(bid_new.order.side, OrderSide::Buy.into());
1817 assert_eq!(bid_new.order.price, instrument.make_price(42498.5));
1818 assert_eq!(bid_new.order.size, instrument.make_qty(300.0, None));
1819
1820 let ask_delete = &deltas.deltas[2];
1822 assert_eq!(ask_delete.action, BookAction::Delete);
1823 assert_eq!(ask_delete.order.side, OrderSide::Sell.into());
1824 assert_eq!(ask_delete.order.price, instrument.make_price(42501.0));
1825
1826 let ask_change = &deltas.deltas[3];
1828 assert_eq!(ask_change.action, BookAction::Update);
1829 assert_eq!(ask_change.order.side, OrderSide::Sell.into());
1830 assert_eq!(ask_change.order.price, instrument.make_price(42501.5));
1831 assert_eq!(ask_change.order.size, instrument.make_qty(700.0, None));
1832 }
1833
1834 #[rstest]
1835 fn test_parse_book_grouped_snapshot() {
1836 let instrument = test_perpetual_instrument();
1839 let json = load_test_json("ws_book_grouped_snapshot.json");
1840 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1841 let data = response["params"]["data"].to_string();
1842 let msg: DeribitBookMsg = serde_json::from_str(&data).unwrap();
1843
1844 assert_eq!(
1846 msg.bids[0].len(),
1847 2,
1848 "Grouped bids should have 2 elements: [price, amount]"
1849 );
1850 assert_eq!(
1851 msg.asks[0].len(),
1852 2,
1853 "Grouped asks should have 2 elements: [price, amount]"
1854 );
1855
1856 assert_eq!(
1858 msg.msg_type,
1859 DeribitBookMsgType::Snapshot,
1860 "Grouped channel should default to Snapshot type"
1861 );
1862
1863 let deltas = parse_book_snapshot(&msg, &instrument, UnixNanos::default()).unwrap();
1864
1865 assert_eq!(deltas.instrument_id, instrument.id());
1866 assert_eq!(deltas.deltas.len(), 21);
1868
1869 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
1871
1872 let first_bid = &deltas.deltas[1];
1874 assert_eq!(first_bid.action, BookAction::Add);
1875 assert_eq!(first_bid.order.side, OrderSide::Buy.into());
1876 assert_eq!(first_bid.order.price, instrument.make_price(89532.5));
1877 assert_eq!(first_bid.order.size, instrument.make_qty(254900.0, None));
1878
1879 let first_ask = &deltas.deltas[11];
1881 assert_eq!(first_ask.action, BookAction::Add);
1882 assert_eq!(first_ask.order.side, OrderSide::Sell.into());
1883 assert_eq!(first_ask.order.price, instrument.make_price(89533.0));
1884 assert_eq!(first_ask.order.size, instrument.make_qty(91570.0, None));
1885
1886 let last = deltas.deltas.last().unwrap();
1888 assert_eq!(
1889 last.flags & RecordFlag::F_LAST as u8,
1890 RecordFlag::F_LAST as u8
1891 );
1892 }
1893
1894 #[rstest]
1895 fn test_parse_ticker_to_mark_price() {
1896 let instrument = test_perpetual_instrument();
1897 let json = load_test_json("ws_ticker.json");
1898 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1899 let msg: DeribitTickerMsg =
1900 serde_json::from_str(&response["params"]["data"].to_string()).unwrap();
1901
1902 let mark_price =
1903 parse_ticker_to_mark_price(&msg, &instrument, UnixNanos::default()).unwrap();
1904
1905 assert_eq!(mark_price.instrument_id, instrument.id());
1906 assert_eq!(mark_price.value, instrument.make_price(92281.78));
1907 assert_eq!(
1908 mark_price.ts_event,
1909 UnixNanos::new(1_765_541_474_086_000_000)
1910 );
1911 }
1912
1913 #[rstest]
1914 fn test_parse_ticker_to_index_price() {
1915 let instrument = test_perpetual_instrument();
1916 let json = load_test_json("ws_ticker.json");
1917 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1918 let msg: DeribitTickerMsg =
1919 serde_json::from_str(&response["params"]["data"].to_string()).unwrap();
1920
1921 let index_price =
1922 parse_ticker_to_index_price(&msg, &instrument, UnixNanos::default()).unwrap();
1923
1924 assert_eq!(index_price.instrument_id, instrument.id());
1925 assert_eq!(index_price.value, instrument.make_price(92263.55));
1926 assert_eq!(
1927 index_price.ts_event,
1928 UnixNanos::new(1_765_541_474_086_000_000)
1929 );
1930 }
1931
1932 #[rstest]
1933 fn test_parse_ticker_to_funding_rate() {
1934 let instrument = test_perpetual_instrument();
1935 let json = load_test_json("ws_ticker.json");
1936 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1937 let msg: DeribitTickerMsg =
1938 serde_json::from_str(&response["params"]["data"].to_string()).unwrap();
1939
1940 assert!(msg.current_funding.is_some());
1942
1943 let funding_rate =
1944 parse_ticker_to_funding_rate(&msg, &instrument, UnixNanos::default()).unwrap();
1945
1946 assert_eq!(funding_rate.instrument_id, instrument.id());
1947 assert_eq!(
1949 funding_rate.ts_event,
1950 UnixNanos::new(1_765_541_474_086_000_000)
1951 );
1952 assert!(funding_rate.interval.is_none());
1953 assert!(funding_rate.next_funding_ns.is_none()); }
1955
1956 #[rstest]
1957 fn test_resolution_to_bar_type_1_minute() {
1958 let instrument_id = InstrumentId::from("BTC-PERPETUAL.DERIBIT");
1959 let bar_type = resolution_to_bar_type(instrument_id, "1").unwrap();
1960
1961 assert_eq!(bar_type.instrument_id(), instrument_id);
1962 assert_eq!(bar_type.spec().step.get(), 1);
1963 assert_eq!(bar_type.spec().aggregation, BarAggregation::Minute);
1964 assert_eq!(bar_type.spec().price_type, PriceType::Last);
1965 assert_eq!(bar_type.aggregation_source(), AggregationSource::External);
1966 }
1967
1968 #[rstest]
1969 fn test_resolution_to_bar_type_60_minute() {
1970 let instrument_id = InstrumentId::from("ETH-PERPETUAL.DERIBIT");
1971 let bar_type = resolution_to_bar_type(instrument_id, "60").unwrap();
1972
1973 assert_eq!(bar_type.instrument_id(), instrument_id);
1974 assert_eq!(bar_type.spec().step.get(), 1);
1975 assert_eq!(bar_type.spec().aggregation, BarAggregation::Hour);
1976 }
1977
1978 #[rstest]
1979 fn test_resolution_to_bar_type_daily() {
1980 let instrument_id = InstrumentId::from("BTC-PERPETUAL.DERIBIT");
1981 let bar_type = resolution_to_bar_type(instrument_id, "1D").unwrap();
1982
1983 assert_eq!(bar_type.instrument_id(), instrument_id);
1984 assert_eq!(bar_type.spec().step.get(), 1);
1985 assert_eq!(bar_type.spec().aggregation, BarAggregation::Day);
1986 }
1987
1988 #[rstest]
1989 fn test_resolution_to_bar_type_invalid() {
1990 let instrument_id = InstrumentId::from("BTC-PERPETUAL.DERIBIT");
1991 let result = resolution_to_bar_type(instrument_id, "invalid");
1992
1993 assert!(result.is_err());
1994 assert!(
1995 result
1996 .unwrap_err()
1997 .to_string()
1998 .contains("Unsupported Deribit resolution")
1999 );
2000 }
2001
2002 #[rstest]
2003 fn test_parse_chart_msg_uses_cost() {
2004 let instrument = test_perpetual_instrument();
2005 assert!(
2006 instrument.is_inverse(),
2007 "test fixture is expected to be an inverse perp"
2008 );
2009
2010 let json = load_test_json("ws_chart.json");
2011 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
2012 let chart_msg: DeribitChartMsg =
2013 serde_json::from_str(&response["params"]["data"].to_string()).unwrap();
2014
2015 assert_eq!(chart_msg.tick, 1_767_200_040_000);
2017 assert_eq!(chart_msg.open, dec!(87490.0));
2018 assert_eq!(chart_msg.high, dec!(87500.0));
2019 assert_eq!(chart_msg.low, dec!(87465.0));
2020 assert_eq!(chart_msg.close, dec!(87474.0));
2021 assert_eq!(chart_msg.volume, dec!(0.95978896));
2022 assert_eq!(chart_msg.cost, dec!(83970.0));
2023
2024 let bar_type = resolution_to_bar_type(instrument.id(), "1").unwrap();
2025
2026 let bar = parse_chart_msg(
2028 &chart_msg,
2029 bar_type,
2030 instrument.price_precision(),
2031 instrument.size_precision(),
2032 true, true,
2034 UnixNanos::default(),
2035 )
2036 .unwrap();
2037
2038 assert_eq!(bar.bar_type, bar_type);
2039 assert_eq!(bar.open, instrument.make_price(87490.0));
2040 assert_eq!(bar.high, instrument.make_price(87500.0));
2041 assert_eq!(bar.low, instrument.make_price(87465.0));
2042 assert_eq!(bar.close, instrument.make_price(87474.0));
2043 assert_eq!(bar.volume, instrument.make_qty(83970.0, None));
2044
2045 assert_eq!(bar.ts_event, UnixNanos::new(1_767_200_100_000_000_000));
2047 }
2048
2049 #[rstest]
2050 fn test_parse_order_buy_response() {
2051 let instrument = test_perpetual_instrument();
2052 let json = load_test_json("ws_order_buy_response.json");
2053 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
2054
2055 let order_msg: DeribitOrderMsg =
2057 serde_json::from_str(&response["result"]["order"].to_string()).unwrap();
2058
2059 assert_eq!(order_msg.order_id, "USDC-104819327443");
2061 assert_eq!(
2062 order_msg.label,
2063 Some("O-19700101-000000-001-001-1".to_string())
2064 );
2065 assert_eq!(order_msg.direction, "buy");
2066 assert_eq!(order_msg.order_state, "open");
2067 assert_eq!(order_msg.order_type, "limit");
2068 assert_eq!(order_msg.price, Some(dec!(2973.55)));
2069 assert_eq!(order_msg.amount, dec!(0.001));
2070 assert_eq!(order_msg.filled_amount, rust_decimal::Decimal::ZERO);
2071 assert!(order_msg.post_only);
2072 assert!(!order_msg.reduce_only);
2073
2074 let account_id = AccountId::new("DERIBIT-001");
2076 let trader_id = TraderId::new("TRADER-001");
2077 let strategy_id = StrategyId::new("PMM-001");
2078
2079 let accepted = parse_order_accepted(
2080 &order_msg,
2081 &instrument,
2082 account_id,
2083 trader_id,
2084 strategy_id,
2085 UnixNanos::default(),
2086 );
2087
2088 assert_eq!(
2089 accepted.client_order_id.to_string(),
2090 "O-19700101-000000-001-001-1"
2091 );
2092 assert_eq!(accepted.venue_order_id.to_string(), "USDC-104819327443");
2093 assert_eq!(accepted.trader_id, trader_id);
2094 assert_eq!(accepted.strategy_id, strategy_id);
2095 assert_eq!(accepted.account_id, account_id);
2096 }
2097
2098 #[rstest]
2099 fn test_parse_order_sell_response() {
2100 let instrument = test_perpetual_instrument();
2101 let json = load_test_json("ws_order_sell_response.json");
2102 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
2103
2104 let order_msg: DeribitOrderMsg =
2105 serde_json::from_str(&response["result"]["order"].to_string()).unwrap();
2106
2107 assert_eq!(order_msg.order_id, "USDC-104819327458");
2109 assert_eq!(
2110 order_msg.label,
2111 Some("O-19700101-000000-001-001-2".to_string())
2112 );
2113 assert_eq!(order_msg.direction, "sell");
2114 assert_eq!(order_msg.order_state, "open");
2115 assert_eq!(order_msg.price, Some(dec!(3286.7)));
2116 assert_eq!(order_msg.amount, dec!(0.001));
2117
2118 let account_id = AccountId::new("DERIBIT-001");
2120 let trader_id = TraderId::new("TRADER-001");
2121 let strategy_id = StrategyId::new("PMM-001");
2122
2123 let accepted = parse_order_accepted(
2124 &order_msg,
2125 &instrument,
2126 account_id,
2127 trader_id,
2128 strategy_id,
2129 UnixNanos::default(),
2130 );
2131
2132 assert_eq!(
2133 accepted.client_order_id.to_string(),
2134 "O-19700101-000000-001-001-2"
2135 );
2136 assert_eq!(accepted.venue_order_id.to_string(), "USDC-104819327458");
2137 assert_eq!(accepted.trader_id, trader_id);
2138 assert_eq!(accepted.strategy_id, strategy_id);
2139 assert_eq!(accepted.account_id, account_id);
2140 }
2141
2142 #[rstest]
2143 fn test_parse_order_edit_response() {
2144 let instrument = test_perpetual_instrument();
2145 let json = load_test_json("ws_order_edit_response.json");
2146 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
2147
2148 let order_msg: DeribitOrderMsg =
2149 serde_json::from_str(&response["result"]["order"].to_string()).unwrap();
2150
2151 assert_eq!(order_msg.order_id, "USDC-104819327443");
2153 assert_eq!(
2154 order_msg.label,
2155 Some("O-19700101-000000-001-001-1".to_string())
2156 );
2157 assert_eq!(order_msg.direction, "buy");
2158 assert_eq!(order_msg.order_state, "open");
2159 assert!(order_msg.replaced);
2160 assert_eq!(order_msg.price, Some(dec!(3067.2))); let account_id = AccountId::new("DERIBIT-001");
2164 let trader_id = TraderId::new("TRADER-001");
2165 let strategy_id = StrategyId::new("PMM-001");
2166
2167 let updated = parse_order_updated(
2168 &order_msg,
2169 &instrument,
2170 account_id,
2171 trader_id,
2172 strategy_id,
2173 UnixNanos::default(),
2174 );
2175
2176 assert_eq!(
2177 updated.client_order_id.to_string(),
2178 "O-19700101-000000-001-001-1"
2179 );
2180 assert_eq!(
2181 updated.venue_order_id.unwrap().to_string(),
2182 "USDC-104819327443"
2183 );
2184 assert_eq!(updated.quantity.as_f64(), 0.0);
2186 }
2187
2188 #[rstest]
2189 fn test_parse_order_cancel_response() {
2190 let instrument = test_perpetual_instrument();
2191 let json = load_test_json("ws_order_cancel_response.json");
2192 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
2193
2194 let order_msg: DeribitOrderMsg =
2196 serde_json::from_str(&response["result"].to_string()).unwrap();
2197
2198 assert_eq!(order_msg.order_id, "USDC-104819327443");
2200 assert_eq!(
2201 order_msg.label,
2202 Some("O-19700101-000000-001-001-1".to_string())
2203 );
2204 assert_eq!(order_msg.order_state, "cancelled");
2205 assert_eq!(order_msg.cancel_reason, Some("user_request".to_string()));
2206
2207 let account_id = AccountId::new("DERIBIT-001");
2209 let trader_id = TraderId::new("TRADER-001");
2210 let strategy_id = StrategyId::new("PMM-001");
2211
2212 let canceled = parse_order_canceled(
2213 &order_msg,
2214 &instrument,
2215 account_id,
2216 trader_id,
2217 strategy_id,
2218 UnixNanos::default(),
2219 );
2220
2221 assert_eq!(
2222 canceled.client_order_id.to_string(),
2223 "O-19700101-000000-001-001-1"
2224 );
2225 assert_eq!(
2226 canceled.venue_order_id.unwrap().to_string(),
2227 "USDC-104819327443"
2228 );
2229 assert_eq!(canceled.trader_id, trader_id);
2230 assert_eq!(canceled.strategy_id, strategy_id);
2231 }
2232
2233 #[rstest]
2234 fn test_parse_order_stop_market_response() {
2235 let instrument = test_perpetual_instrument();
2240 let json = load_test_json("ws_order_stop_market_response.json");
2241 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
2242
2243 let order_msg: DeribitOrderMsg =
2244 serde_json::from_str(&response["result"]["order"].to_string()).unwrap();
2245
2246 assert_eq!(order_msg.order_id, "USDC-104819327499");
2247 assert_eq!(order_msg.order_type, "stop_market");
2248 assert_eq!(order_msg.order_state, "untriggered");
2249 assert_eq!(order_msg.price, None);
2250 assert_eq!(order_msg.trigger_price, Some(dec!(2228.0)));
2251 assert_eq!(order_msg.trigger.as_deref(), Some("mark_price"));
2252 assert!(order_msg.reduce_only);
2253
2254 let account_id = AccountId::new("DERIBIT-001");
2255 let report =
2256 parse_user_order_msg(&order_msg, &instrument, account_id, UnixNanos::default())
2257 .unwrap();
2258
2259 assert_eq!(report.order_type, OrderType::StopMarket);
2260 assert_eq!(report.order_status, OrderStatus::Accepted);
2261 assert!(report.price.is_none());
2262 assert!(report.trigger_price.is_some());
2263 assert!(report.reduce_only);
2264 }
2265
2266 #[rstest]
2267 fn test_parse_order_stop_market_response_missing_filled_amount() {
2268 let instrument = test_perpetual_instrument();
2273 let json = load_test_json("ws_order_stop_market_no_filled_amount.json");
2274 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
2275
2276 let order_msg: DeribitOrderMsg =
2277 serde_json::from_str(&response["result"]["order"].to_string()).unwrap();
2278
2279 assert_eq!(order_msg.order_id, "USDC-SLMB-19641");
2280 assert_eq!(order_msg.order_type, "stop_market");
2281 assert_eq!(order_msg.order_state, "untriggered");
2282 assert_eq!(order_msg.filled_amount, rust_decimal::Decimal::ZERO);
2283 assert_eq!(order_msg.average_price, None);
2284
2285 let account_id = AccountId::new("DERIBIT-001");
2286 let report =
2287 parse_user_order_msg(&order_msg, &instrument, account_id, UnixNanos::default())
2288 .unwrap();
2289
2290 assert_eq!(report.order_type, OrderType::StopMarket);
2291 assert_eq!(report.order_status, OrderStatus::Accepted);
2292 assert_eq!(report.filled_qty.as_f64(), 0.0);
2293 }
2294
2295 #[rstest]
2296 fn test_parse_user_order_msg_to_status_report() {
2297 let instrument = test_perpetual_instrument();
2298 let json = load_test_json("ws_order_buy_response.json");
2299 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
2300
2301 let order_msg: DeribitOrderMsg =
2302 serde_json::from_str(&response["result"]["order"].to_string()).unwrap();
2303
2304 let account_id = AccountId::new("DERIBIT-001");
2305 let report =
2306 parse_user_order_msg(&order_msg, &instrument, account_id, UnixNanos::default())
2307 .unwrap();
2308
2309 assert_eq!(report.venue_order_id.to_string(), "USDC-104819327443");
2310 assert_eq!(
2311 report.client_order_id.unwrap().to_string(),
2312 "O-19700101-000000-001-001-1"
2313 );
2314 assert_eq!(report.order_side, OrderSide::Buy.into());
2315 assert_eq!(report.order_type, OrderType::Limit);
2316 assert_eq!(report.time_in_force, TimeInForce::Gtc);
2317 assert_eq!(report.order_status, OrderStatus::Accepted);
2318 assert_eq!(report.quantity.as_f64(), 0.0);
2320 assert_eq!(report.filled_qty.as_f64(), 0.0);
2321 assert!(report.post_only);
2322 assert!(!report.reduce_only);
2323 }
2324
2325 #[rstest]
2326 fn test_determine_order_event_type() {
2327 assert_eq!(
2329 determine_order_event_type("open", true, false),
2330 OrderEventType::Accepted
2331 );
2332
2333 assert_eq!(
2335 determine_order_event_type("open", false, true),
2336 OrderEventType::Updated
2337 );
2338
2339 assert_eq!(
2341 determine_order_event_type("cancelled", false, false),
2342 OrderEventType::Canceled
2343 );
2344
2345 assert_eq!(
2347 determine_order_event_type("expired", false, false),
2348 OrderEventType::Expired
2349 );
2350
2351 assert_eq!(
2353 determine_order_event_type("filled", false, false),
2354 OrderEventType::None
2355 );
2356 }
2357
2358 fn perpetual_instrument_with_increments(
2359 tick_size: &str,
2360 min_trade_amount: &str,
2361 ) -> InstrumentAny {
2362 let json = load_test_json("http_get_instruments.json");
2363 let mut response: serde_json::Value = serde_json::from_str(&json).unwrap();
2364 response["result"][0]["tick_size"] = serde_json::json!(tick_size);
2365 response["result"][0]["min_trade_amount"] = serde_json::json!(min_trade_amount);
2366 let response: DeribitJsonRpcResponse<Vec<DeribitInstrument>> =
2367 serde_json::from_str(&response.to_string()).unwrap();
2368 let instrument = &response.result.unwrap()[0];
2369 parse_deribit_instrument_any(instrument, UnixNanos::default(), UnixNanos::default())
2370 .unwrap()
2371 .unwrap()
2372 }
2373
2374 fn parse_notification_data<T: DeserializeOwned>(fixture: &str) -> T {
2375 serde_json::from_str(¬ification_data(fixture)).unwrap()
2376 }
2377
2378 fn notification_data(fixture: &str) -> String {
2379 let json = load_test_json(fixture);
2380
2381 let DeribitWsMessage::Notification(notification) = parse_raw_message(&json).unwrap() else {
2382 panic!("Expected notification from {fixture}");
2383 };
2384
2385 notification.params.data.get().to_string()
2386 }
2387
2388 #[rstest]
2389 fn test_parse_user_trade_msg_keeps_decimals_that_collapse_in_f64() {
2390 let instrument = perpetual_instrument_with_increments("0.000000001", "0.000000001");
2391 let trades: Vec<DeribitUserTradeMsg> = parse_notification_data("ws_user_trades_exact.json");
2392 let account_id = AccountId::new("DERIBIT-001");
2393 let btc = Currency::BTC();
2394
2395 let first = parse_user_trade_msg(&trades[0], &instrument, account_id, UnixNanos::default())
2396 .unwrap();
2397 let second =
2398 parse_user_trade_msg(&trades[1], &instrument, account_id, UnixNanos::default())
2399 .unwrap();
2400
2401 let fee_f64 = "1000000000.12345678".parse::<f64>().unwrap();
2402 let next_fee_f64 = "1000000000.12345679".parse::<f64>().unwrap();
2403 assert_eq!(fee_f64.to_bits(), next_fee_f64.to_bits());
2404 assert_eq!(instrument.price_precision(), 9);
2405 assert_eq!(instrument.size_precision(), 9);
2406 assert_eq!(first.last_px, Price::from("100000000.123456789"));
2407 assert_eq!(first.last_qty, Quantity::from("100000000.123456789"));
2408 assert_eq!(
2409 first.commission,
2410 Money::from_decimal(dec!(1000000000.12345678), btc).unwrap()
2411 );
2412 assert_eq!(second.last_px, Price::from("100000000.123456788"));
2413 assert_eq!(second.last_qty, Quantity::from("100000000.123456788"));
2414 assert_eq!(
2415 second.commission,
2416 Money::from_decimal(dec!(1000000000.12345679), btc).unwrap()
2417 );
2418 assert_eq!(trades[0].mark_price, dec!(100000000.123456788));
2419 assert_eq!(trades[0].profit_loss, Some(dec!(0.30000000000000004)));
2420 }
2421
2422 #[rstest]
2423 fn test_parse_user_trade_msg_rounds_exact_token_half_even() {
2424 let instrument = perpetual_instrument_with_increments("0.01", "0.01");
2425 let trades: Vec<DeribitUserTradeMsg> = parse_notification_data("ws_user_trades_exact.json");
2426 let account_id = AccountId::new("DERIBIT-001");
2427
2428 let fill = parse_user_trade_msg(&trades[2], &instrument, account_id, UnixNanos::default())
2429 .unwrap();
2430
2431 let token_f64 = "100000000.005000001".parse::<f64>().unwrap();
2432 let via_f64 = Price::from_decimal_dp(Decimal::try_from(token_f64).unwrap(), 2).unwrap();
2433 assert_eq!(via_f64, Price::from("100000000.00"));
2434 assert_eq!(fill.last_px, Price::from("100000000.01"));
2435 assert_eq!(fill.last_qty, Quantity::from("100000000.01"));
2436 assert_eq!(
2437 fill.commission,
2438 Money::from_decimal(dec!(0.00011403), Currency::BTC()).unwrap()
2439 );
2440 }
2441
2442 #[rstest]
2443 fn test_parse_portfolio_keeps_decimals_that_collapse_in_f64() {
2444 let portfolio: DeribitPortfolioMsg = parse_notification_data("ws_portfolio_exact.json");
2445 let account_id = AccountId::new("DERIBIT-001");
2446 let btc = Currency::BTC();
2447
2448 let state =
2449 parse_portfolio_to_account_state(&portfolio, account_id, UnixNanos::default()).unwrap();
2450
2451 assert_eq!(state.balances.len(), 1);
2452 assert_eq!(
2453 state.balances[0].total,
2454 Money::from_decimal(dec!(1000000000.12345679), btc).unwrap()
2455 );
2456 assert_eq!(
2457 state.balances[0].free,
2458 Money::from_decimal(dec!(1000000000.12345678), btc).unwrap()
2459 );
2460 assert_eq!(
2461 state.balances[0].locked,
2462 Money::from_decimal(dec!(0.00000001), btc).unwrap()
2463 );
2464 assert_eq!(state.margins.len(), 1);
2465 assert_eq!(
2466 state.margins[0].initial,
2467 Money::from_decimal(dec!(1000000000.12345678), btc).unwrap()
2468 );
2469 assert_eq!(state.margins[0].maintenance, Money::zero(btc));
2470 assert_eq!(
2471 portfolio.maintenance_margin,
2472 dec!(0.0000000000000000555111512313)
2473 );
2474 }
2475
2476 #[rstest]
2477 fn test_parse_book_snapshot_keeps_decimals_that_collapse_in_f64() {
2478 let instrument = perpetual_instrument_with_increments("0.000000001", "0.000000001");
2479 let data = notification_data("ws_book_snapshot_exact.json");
2480 let msg: DeribitBookMsg = serde_json::from_str(&data).unwrap();
2481
2482 let deltas = parse_book_msg(&msg, &instrument, UnixNanos::default()).unwrap();
2483
2484 assert_eq!(deltas.deltas.len(), 3);
2485 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
2486 assert_eq!(
2487 deltas.deltas[1].order.price,
2488 Price::from("100000000.123456789")
2489 );
2490 assert_eq!(
2491 deltas.deltas[1].order.size,
2492 Quantity::from("100000000.123456789")
2493 );
2494 assert_eq!(
2495 deltas.deltas[2].order.price,
2496 Price::from("100000000.123456788")
2497 );
2498 assert_eq!(
2499 deltas.deltas[2].order.size,
2500 Quantity::from("100000000.123456788")
2501 );
2502 }
2503
2504 #[rstest]
2505 fn test_parse_book_delta_rounds_levels_beyond_decimal_scale() {
2506 let instrument = perpetual_instrument_with_increments("0.000000001", "0.000000001");
2507 let data = notification_data("ws_book_delta_exact.json");
2508 let msg: DeribitBookMsg = serde_json::from_str(&data).unwrap();
2509
2510 let deltas = parse_book_msg(&msg, &instrument, UnixNanos::default()).unwrap();
2511
2512 assert_eq!(deltas.deltas.len(), 2);
2513 assert_eq!(deltas.deltas[0].action, BookAction::Update);
2514 assert_eq!(
2515 deltas.deltas[0].order.price,
2516 Price::from("100000000.123456789")
2517 );
2518 assert_eq!(deltas.deltas[0].order.size, Quantity::from("0.123456789"));
2519 assert_eq!(deltas.deltas[1].action, BookAction::Add);
2520 assert_eq!(
2521 deltas.deltas[1].order.price,
2522 Price::from("100000000.123456788")
2523 );
2524 assert_eq!(deltas.deltas[1].order.size, Quantity::from("0.000012346"));
2525 }
2526
2527 #[rstest]
2528 fn test_parse_book_delta_maps_zero_rounding_amounts_to_delete() {
2529 let instrument = perpetual_instrument_with_increments("0.5", "1");
2530 let data = r#"{"type":"change","instrument_name":"BTC-PERPETUAL","timestamp":1700000000000,
2531 "change_id":101,"prev_change_id":100,
2532 "bids":[["change",42500.0,1.1102230246251565e-16],["new",42499.5,0.4]],
2533 "asks":[["change",42500.5,20.0],["delete",42501.0,0.0]]}"#;
2534 let msg: DeribitBookMsg = serde_json::from_str(data).unwrap();
2535
2536 let deltas = parse_book_msg(&msg, &instrument, UnixNanos::default()).unwrap();
2537
2538 let actual: Vec<_> = deltas
2539 .deltas
2540 .iter()
2541 .map(|delta| {
2542 (
2543 delta.action,
2544 delta.order.side,
2545 delta.order.price,
2546 delta.order.size,
2547 delta.order.order_id,
2548 delta.flags,
2549 delta.sequence,
2550 )
2551 })
2552 .collect();
2553
2554 assert_eq!(
2555 actual,
2556 vec![
2557 (
2558 BookAction::Delete,
2559 Some(OrderSide::Buy),
2560 Price::from("42500.0"),
2561 Quantity::from("0"),
2562 0,
2563 0,
2564 101,
2565 ),
2566 (
2567 BookAction::Delete,
2568 Some(OrderSide::Buy),
2569 Price::from("42499.5"),
2570 Quantity::from("0"),
2571 1,
2572 0,
2573 101,
2574 ),
2575 (
2576 BookAction::Update,
2577 Some(OrderSide::Sell),
2578 Price::from("42500.5"),
2579 Quantity::from("20"),
2580 2,
2581 0,
2582 101,
2583 ),
2584 (
2585 BookAction::Delete,
2586 Some(OrderSide::Sell),
2587 Price::from("42501.0"),
2588 Quantity::from("0"),
2589 3,
2590 RecordFlag::F_LAST as u8,
2591 101,
2592 ),
2593 ]
2594 );
2595 }
2596
2597 #[rstest]
2598 fn test_parse_book_snapshot_skips_zero_rounding_amounts() {
2599 let instrument = perpetual_instrument_with_increments("0.5", "1");
2600 let data = r#"{"type":"snapshot","instrument_name":"BTC-PERPETUAL","timestamp":1700000000000,
2601 "change_id":100,
2602 "bids":[["new",42500.0,1.1102230246251565e-16],["new",42499.5,10.0]],
2603 "asks":[["new",42500.5,0.4]]}"#;
2604 let msg: DeribitBookMsg = serde_json::from_str(data).unwrap();
2605
2606 let deltas = parse_book_msg(&msg, &instrument, UnixNanos::default()).unwrap();
2607
2608 let actual: Vec<_> = deltas
2609 .deltas
2610 .iter()
2611 .map(|delta| {
2612 (
2613 delta.action,
2614 delta.order.side,
2615 delta.order.price,
2616 delta.order.size,
2617 delta.order.order_id,
2618 delta.flags,
2619 )
2620 })
2621 .collect();
2622
2623 assert_eq!(
2624 actual,
2625 vec![
2626 (
2627 BookAction::Clear,
2628 None,
2629 Price::from("0"),
2630 Quantity::from("0"),
2631 0,
2632 RecordFlag::F_SNAPSHOT as u8,
2633 ),
2634 (
2635 BookAction::Add,
2636 Some(OrderSide::Buy),
2637 Price::from("42499.5"),
2638 Quantity::from("10"),
2639 1,
2640 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8,
2641 ),
2642 ]
2643 );
2644 }
2645
2646 #[rstest]
2647 #[case::snapshot(
2648 r#"{"type":"snapshot","instrument_name":"BTC-PERPETUAL","timestamp":1700000000000,
2649 "change_id":100,"bids":[["new",100000000000000000000.0,10.0]],"asks":[]}"#
2650 )]
2651 #[case::delta(
2652 r#"{"type":"change","instrument_name":"BTC-PERPETUAL","timestamp":1700000000000,
2653 "change_id":101,"prev_change_id":100,
2654 "bids":[["new",100000000000000000000.0,10.0]],"asks":[]}"#
2655 )]
2656 fn test_parse_book_msg_rejects_unconvertible_price(#[case] data: &str) {
2657 let instrument = perpetual_instrument_with_increments("0.5", "1");
2658 let msg: DeribitBookMsg = serde_json::from_str(data).unwrap();
2659
2660 let error = parse_book_msg(&msg, &instrument, UnixNanos::default()).unwrap_err();
2661
2662 let expected = Price::from_decimal_dp(dec!(100000000000000000000.0), 1).unwrap_err();
2663 assert_eq!(error.to_string(), expected.to_string());
2664 }
2665}