1use ahash::AHashMap;
19use anyhow::Context;
20use chrono::{Duration, TimeZone, Timelike, Utc};
21use nautilus_core::{UUID4, UnixNanos, datetime::NANOSECONDS_IN_MILLISECOND};
22use nautilus_model::{
23 data::{
24 Bar, BarType, BookOrder, Data, FundingRateUpdate, IndexPriceUpdate, MarkPriceUpdate,
25 OrderBookDelta, OrderBookDeltas, QuoteTick, TradeTick, bar::BarSpecification,
26 option_chain::OptionGreeks,
27 },
28 enums::{
29 AggregationSource, AggressorSide, BarAggregation, BookAction, GreeksConvention,
30 LiquiditySide, OrderSide, OrderStatus, OrderType, PositionSideSpecified, PriceType,
31 RecordFlag, TimeInForce,
32 },
33 events::{OrderAccepted, OrderCanceled, OrderExpired, OrderUpdated},
34 identifiers::{
35 AccountId, ClientOrderId, InstrumentId, StrategyId, TradeId, TraderId, VenueOrderId,
36 },
37 instruments::{Instrument, InstrumentAny},
38 reports::{FillReport, OrderStatusReport, PositionStatusReport},
39 types::{Currency, Money, Price, Quantity},
40};
41use rust_decimal::prelude::ToPrimitive;
42use ustr::Ustr;
43
44use super::{
45 enums::{DeribitBookAction, DeribitBookMsgType},
46 messages::{
47 DeribitBookMsg, DeribitChartMsg, DeribitOrderMsg, DeribitPerpetualMsg, DeribitQuoteMsg,
48 DeribitTickerMsg, DeribitTradeMsg, DeribitUserTradeMsg,
49 },
50};
51use crate::http::models::DeribitPosition;
52
53fn next_8_utc(from_ns: UnixNanos) -> anyhow::Result<UnixNanos> {
54 let from_secs = from_ns.as_u64() / 1_000_000_000;
55 let dt = Utc
56 .timestamp_opt(from_secs as i64, 0)
57 .single()
58 .context("failed to convert timestamp to UTC datetime")?;
59 let next_8 = if dt.hour() < 8 {
60 dt.date_naive()
61 .and_hms_opt(8, 0, 0)
62 .context("failed to construct 08:00 UTC time")?
63 .and_utc()
64 } else {
65 (dt.date_naive() + Duration::days(1))
66 .and_hms_opt(8, 0, 0)
67 .context("failed to construct next-day 08:00 UTC time")?
68 .and_utc()
69 };
70 let nanos = next_8
71 .timestamp_nanos_opt()
72 .context("GTD expiry timestamp out of nanosecond range")?;
73 Ok(UnixNanos::from(nanos as u64))
74}
75
76pub fn parse_trade_msg(
82 msg: &DeribitTradeMsg,
83 instrument: &InstrumentAny,
84 ts_init: UnixNanos,
85) -> anyhow::Result<TradeTick> {
86 let instrument_id = instrument.id();
87 let price_precision = instrument.price_precision();
88 let size_precision = instrument.size_precision();
89
90 let price = Price::from_decimal_dp(msg.price, price_precision)?;
91 let size = Quantity::from_decimal_dp(msg.amount.abs(), size_precision)?;
92
93 let aggressor_side = match msg.direction.as_str() {
94 "buy" => AggressorSide::Buyer,
95 "sell" => AggressorSide::Seller,
96 _ => AggressorSide::NoAggressor,
97 };
98
99 let trade_id = TradeId::new(&msg.trade_id);
100 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
101
102 TradeTick::new_checked(
103 instrument_id,
104 price,
105 size,
106 aggressor_side,
107 trade_id,
108 ts_event,
109 ts_init,
110 )
111}
112
113pub fn parse_trades_data(
115 trades: &[DeribitTradeMsg],
116 instruments_cache: &AHashMap<Ustr, InstrumentAny>,
117 ts_init: UnixNanos,
118) -> Vec<Data> {
119 trades
120 .iter()
121 .filter_map(|msg| {
122 instruments_cache
123 .get(&msg.instrument_name)
124 .and_then(|inst| parse_trade_msg(msg, inst, ts_init).ok())
125 .map(Data::Trade)
126 })
127 .collect()
128}
129
130fn parse_snapshot_level(
131 level: &[serde_json::Value],
132 index: usize,
133 side: &str,
134 instrument_name: &str,
135) -> Option<(f64, f64)> {
136 let (price_val, amount_val) = if level.len() >= 3 {
137 let price = level[1].as_f64().or_else(|| {
138 log::warn!(
139 "Failed to parse {side} price at index {index} for {instrument_name}: {level:?}"
140 );
141 None
142 })?;
143 let amount = level[2].as_f64().or_else(|| {
144 log::warn!(
145 "Failed to parse {side} amount at index {index} for {instrument_name}: {level:?}"
146 );
147 None
148 })?;
149 (price, amount)
150 } else if level.len() >= 2 {
151 let price = level[0].as_f64().or_else(|| {
152 log::warn!(
153 "Failed to parse {side} price at index {index} for {instrument_name}: {level:?}"
154 );
155 None
156 })?;
157 let amount = level[1].as_f64().or_else(|| {
158 log::warn!(
159 "Failed to parse {side} amount at index {index} for {instrument_name}: {level:?}"
160 );
161 None
162 })?;
163 (price, amount)
164 } else {
165 log::warn!(
166 "Invalid {side} format at index {index} for {instrument_name}: expected 2-3 elements, was {}",
167 level.len()
168 );
169 return None;
170 };
171
172 if price_val <= 0.0 {
173 log::warn!(
174 "Invalid {side} price {price_val} at index {index} for {instrument_name}: {level:?}"
175 );
176 return None;
177 }
178
179 Some((price_val, amount_val))
180}
181
182fn parse_delta_level(
183 level: &[serde_json::Value],
184 index: usize,
185 side: &str,
186 instrument_name: &str,
187) -> Option<(BookAction, f64, f64)> {
188 if level.len() < 3 {
189 log::warn!(
190 "Invalid {side} delta format at index {index} for {instrument_name}: expected 3 elements, was {}",
191 level.len()
192 );
193 return None;
194 }
195
196 let action_str = level[0].as_str().or_else(|| {
197 log::warn!(
198 "Failed to parse {side} action at index {index} for {instrument_name}: {level:?}"
199 );
200 None
201 })?;
202
203 let deribit_action: DeribitBookAction = action_str.parse().ok().or_else(|| {
204 log::warn!(
205 "Unknown {side} action '{action_str}' at index {index} for {instrument_name}: {level:?}"
206 );
207 None
208 })?;
209
210 let price_val = level[1].as_f64().or_else(|| {
211 log::warn!(
212 "Failed to parse {side} price at index {index} for {instrument_name}: {level:?}"
213 );
214 None
215 })?;
216
217 let amount_val = level[2].as_f64().or_else(|| {
218 log::warn!(
219 "Failed to parse {side} amount at index {index} for {instrument_name}: {level:?}"
220 );
221 None
222 })?;
223
224 if price_val <= 0.0 {
225 log::warn!(
226 "Invalid {side} price {price_val} at index {index} for {instrument_name}: {level:?}"
227 );
228 return None;
229 }
230
231 Some((deribit_action.into(), price_val, amount_val))
232}
233
234pub fn parse_book_snapshot(
240 msg: &DeribitBookMsg,
241 instrument: &InstrumentAny,
242 ts_init: UnixNanos,
243) -> anyhow::Result<OrderBookDeltas> {
244 let instrument_id = instrument.id();
245 let price_precision = instrument.price_precision();
246 let size_precision = instrument.size_precision();
247 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
248
249 let mut deltas = Vec::new();
250
251 let has_levels = !msg.bids.is_empty() || !msg.asks.is_empty();
252
253 let clear_flags = if has_levels {
255 RecordFlag::F_SNAPSHOT as u8
256 } else {
257 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
258 };
259
260 deltas.push(OrderBookDelta::new(
261 instrument_id,
262 BookAction::Clear,
263 BookOrder::default(),
264 clear_flags,
265 msg.change_id,
266 ts_event,
267 ts_init,
268 ));
269
270 for (i, bid) in msg.bids.iter().enumerate() {
271 let Some((price_val, amount_val)) =
272 parse_snapshot_level(bid, i, "bid", msg.instrument_name.as_str())
273 else {
274 continue;
275 };
276
277 if amount_val > 0.0 {
278 let price = Price::new(price_val, price_precision);
279 let size = Quantity::new(amount_val, size_precision);
280
281 deltas.push(OrderBookDelta::new(
282 instrument_id,
283 BookAction::Add,
284 BookOrder::new(OrderSide::Buy, price, size, i as u64),
285 RecordFlag::F_SNAPSHOT as u8,
286 msg.change_id,
287 ts_event,
288 ts_init,
289 ));
290 }
291 }
292
293 let num_bids = msg.bids.len();
294 for (i, ask) in msg.asks.iter().enumerate() {
295 let Some((price_val, amount_val)) =
296 parse_snapshot_level(ask, i, "ask", msg.instrument_name.as_str())
297 else {
298 continue;
299 };
300
301 if amount_val > 0.0 {
302 let price = Price::new(price_val, price_precision);
303 let size = Quantity::new(amount_val, size_precision);
304
305 deltas.push(OrderBookDelta::new(
306 instrument_id,
307 BookAction::Add,
308 BookOrder::new(OrderSide::Sell, price, size, (num_bids + i) as u64),
309 RecordFlag::F_SNAPSHOT as u8,
310 msg.change_id,
311 ts_event,
312 ts_init,
313 ));
314 }
315 }
316
317 if let Some(last) = deltas.last_mut() {
318 *last = OrderBookDelta::new(
319 last.instrument_id,
320 last.action,
321 last.order,
322 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8,
323 last.sequence,
324 last.ts_event,
325 last.ts_init,
326 );
327 }
328
329 Ok(OrderBookDeltas::new(instrument_id, deltas))
330}
331
332pub fn parse_book_delta(
338 msg: &DeribitBookMsg,
339 instrument: &InstrumentAny,
340 ts_init: UnixNanos,
341) -> anyhow::Result<OrderBookDeltas> {
342 let instrument_id = instrument.id();
343 let price_precision = instrument.price_precision();
344 let size_precision = instrument.size_precision();
345 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
346
347 let mut deltas = Vec::new();
348
349 for (i, bid) in msg.bids.iter().enumerate() {
350 let Some((action, price_val, amount_val)) =
351 parse_delta_level(bid, i, "bid", msg.instrument_name.as_str())
352 else {
353 continue;
354 };
355
356 let price = Price::new(price_val, price_precision);
357 let size = Quantity::new(amount_val.abs(), size_precision);
358
359 deltas.push(OrderBookDelta::new(
360 instrument_id,
361 action,
362 BookOrder::new(OrderSide::Buy, price, size, i as u64),
363 0,
364 msg.change_id,
365 ts_event,
366 ts_init,
367 ));
368 }
369
370 let num_bids = msg.bids.len();
371 for (i, ask) in msg.asks.iter().enumerate() {
372 let Some((action, price_val, amount_val)) =
373 parse_delta_level(ask, i, "ask", msg.instrument_name.as_str())
374 else {
375 continue;
376 };
377
378 let price = Price::new(price_val, price_precision);
379 let size = Quantity::new(amount_val.abs(), size_precision);
380
381 deltas.push(OrderBookDelta::new(
382 instrument_id,
383 action,
384 BookOrder::new(OrderSide::Sell, price, size, (num_bids + i) as u64),
385 0,
386 msg.change_id,
387 ts_event,
388 ts_init,
389 ));
390 }
391
392 if let Some(last) = deltas.last_mut() {
394 *last = OrderBookDelta::new(
395 last.instrument_id,
396 last.action,
397 last.order,
398 RecordFlag::F_LAST as u8,
399 last.sequence,
400 last.ts_event,
401 last.ts_init,
402 );
403 }
404
405 Ok(OrderBookDeltas::new(instrument_id, deltas))
406}
407
408pub fn parse_book_msg(
414 msg: &DeribitBookMsg,
415 instrument: &InstrumentAny,
416 ts_init: UnixNanos,
417) -> anyhow::Result<OrderBookDeltas> {
418 match msg.msg_type {
419 DeribitBookMsgType::Snapshot => parse_book_snapshot(msg, instrument, ts_init),
420 DeribitBookMsgType::Change => parse_book_delta(msg, instrument, ts_init),
421 }
422}
423
424pub fn parse_ticker_to_quote(
430 msg: &DeribitTickerMsg,
431 instrument: &InstrumentAny,
432 ts_init: UnixNanos,
433) -> anyhow::Result<QuoteTick> {
434 let instrument_id = instrument.id();
435 let price_precision = instrument.price_precision();
436 let size_precision = instrument.size_precision();
437
438 let bid_price_val = msg
439 .best_bid_price
440 .context("Missing best_bid_price in ticker")?;
441 let ask_price_val = msg
442 .best_ask_price
443 .context("Missing best_ask_price in ticker")?;
444
445 let bid_price = Price::from_decimal_dp(bid_price_val, price_precision)?;
446 let ask_price = Price::from_decimal_dp(ask_price_val, price_precision)?;
447 let bid_size =
448 Quantity::from_decimal_dp(msg.best_bid_amount.unwrap_or_default(), size_precision)?;
449 let ask_size =
450 Quantity::from_decimal_dp(msg.best_ask_amount.unwrap_or_default(), size_precision)?;
451 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
452
453 QuoteTick::new_checked(
454 instrument_id,
455 bid_price,
456 ask_price,
457 bid_size,
458 ask_size,
459 ts_event,
460 ts_init,
461 )
462}
463
464pub fn parse_quote_msg(
470 msg: &DeribitQuoteMsg,
471 instrument: &InstrumentAny,
472 ts_init: UnixNanos,
473) -> anyhow::Result<QuoteTick> {
474 let instrument_id = instrument.id();
475 let price_precision = instrument.price_precision();
476 let size_precision = instrument.size_precision();
477
478 let bid_price = Price::from_decimal_dp(msg.best_bid_price, price_precision)?;
479 let ask_price = Price::from_decimal_dp(msg.best_ask_price, price_precision)?;
480 let bid_size = Quantity::from_decimal_dp(msg.best_bid_amount, size_precision)?;
481 let ask_size = Quantity::from_decimal_dp(msg.best_ask_amount, size_precision)?;
482 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
483
484 QuoteTick::new_checked(
485 instrument_id,
486 bid_price,
487 ask_price,
488 bid_size,
489 ask_size,
490 ts_event,
491 ts_init,
492 )
493}
494
495pub fn parse_ticker_to_mark_price(
501 msg: &DeribitTickerMsg,
502 instrument: &InstrumentAny,
503 ts_init: UnixNanos,
504) -> anyhow::Result<MarkPriceUpdate> {
505 let instrument_id = instrument.id();
506 let price_precision = instrument.price_precision();
507 let value = Price::from_decimal_dp(msg.mark_price, price_precision)?;
508 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
509
510 Ok(MarkPriceUpdate::new(
511 instrument_id,
512 value,
513 ts_event,
514 ts_init,
515 ))
516}
517
518pub fn parse_ticker_to_index_price(
524 msg: &DeribitTickerMsg,
525 instrument: &InstrumentAny,
526 ts_init: UnixNanos,
527) -> anyhow::Result<IndexPriceUpdate> {
528 let instrument_id = instrument.id();
529 let price_precision = instrument.price_precision();
530 let value = Price::from_decimal_dp(msg.index_price, price_precision)?;
531 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
532
533 Ok(IndexPriceUpdate::new(
534 instrument_id,
535 value,
536 ts_event,
537 ts_init,
538 ))
539}
540
541#[must_use]
545pub fn parse_ticker_to_funding_rate(
546 msg: &DeribitTickerMsg,
547 instrument: &InstrumentAny,
548 ts_init: UnixNanos,
549) -> Option<FundingRateUpdate> {
550 let rate = msg.current_funding?;
552 let instrument_id = instrument.id();
553 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
554
555 Some(FundingRateUpdate::new(
557 instrument_id,
558 rate,
559 None, None, ts_event,
562 ts_init,
563 ))
564}
565
566#[must_use]
570pub fn parse_ticker_to_option_greeks(
571 msg: &DeribitTickerMsg,
572 instrument: &InstrumentAny,
573 ts_init: UnixNanos,
574) -> Option<OptionGreeks> {
575 let deribit_greeks = msg.greeks.as_ref()?;
576 let instrument_id = instrument.id();
577 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
578
579 Some(OptionGreeks {
580 instrument_id,
581 convention: GreeksConvention::BlackScholes,
582 greeks: deribit_greeks.to_greek_values(),
583 mark_iv: msg.mark_iv.and_then(|v| v.to_f64()),
584 bid_iv: msg.bid_iv.and_then(|v| v.to_f64()),
585 ask_iv: msg.ask_iv.and_then(|v| v.to_f64()),
586 underlying_price: msg.underlying_price.and_then(|v| v.to_f64()),
587 open_interest: Some(msg.open_interest.to_f64().unwrap_or(0.0)),
588 ts_event,
589 ts_init,
590 })
591}
592
593#[must_use]
598pub fn parse_perpetual_to_funding_rate(
599 msg: &DeribitPerpetualMsg,
600 instrument: &InstrumentAny,
601 ts_init: UnixNanos,
602) -> FundingRateUpdate {
603 let instrument_id = instrument.id();
604 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
605
606 FundingRateUpdate::new(
607 instrument_id,
608 msg.interest,
609 None, None, ts_event,
612 ts_init,
613 )
614}
615
616pub fn resolution_to_bar_type(
624 instrument_id: InstrumentId,
625 resolution: &str,
626) -> anyhow::Result<BarType> {
627 let (step, aggregation) = match resolution {
628 "1" => (1, BarAggregation::Minute),
629 "3" => (3, BarAggregation::Minute),
630 "5" => (5, BarAggregation::Minute),
631 "10" => (10, BarAggregation::Minute),
632 "15" => (15, BarAggregation::Minute),
633 "30" => (30, BarAggregation::Minute),
634 "60" => (60, BarAggregation::Minute),
635 "120" => (120, BarAggregation::Minute),
636 "180" => (180, BarAggregation::Minute),
637 "360" => (360, BarAggregation::Minute),
638 "720" => (720, BarAggregation::Minute),
639 "1D" => (1, BarAggregation::Day),
640 _ => anyhow::bail!("Unsupported Deribit resolution: {resolution}"),
641 };
642
643 let spec = BarSpecification::new(step, aggregation, PriceType::Last);
644 Ok(BarType::new(
645 instrument_id,
646 spec,
647 AggregationSource::External,
648 ))
649}
650
651pub fn parse_chart_msg(
662 chart_msg: &DeribitChartMsg,
663 bar_type: BarType,
664 price_precision: u8,
665 size_precision: u8,
666 timestamp_on_close: bool,
667 ts_init: UnixNanos,
668) -> anyhow::Result<Bar> {
669 let open = Price::new_checked(chart_msg.open, price_precision).context("Invalid open price")?;
670 let high = Price::new_checked(chart_msg.high, price_precision).context("Invalid high price")?;
671 let low = Price::new_checked(chart_msg.low, price_precision).context("Invalid low price")?;
672 let close =
673 Price::new_checked(chart_msg.close, price_precision).context("Invalid close price")?;
674 let volume =
675 Quantity::new_checked(chart_msg.volume, size_precision).context("Invalid volume")?;
676
677 let mut ts_event = UnixNanos::from(chart_msg.tick * NANOSECONDS_IN_MILLISECOND);
679
680 if timestamp_on_close {
682 let interval_ns = bar_type
683 .spec()
684 .timedelta()
685 .num_nanoseconds()
686 .context("bar specification produced non-integer interval")?;
687 let interval_ns = u64::try_from(interval_ns)
688 .context("bar interval overflowed the u64 range for nanoseconds")?;
689 let updated = ts_event
690 .as_u64()
691 .checked_add(interval_ns)
692 .context("bar timestamp overflowed when adjusting to close time")?;
693 ts_event = UnixNanos::from(updated);
694 }
695
696 Bar::new_checked(bar_type, open, high, low, close, volume, ts_event, ts_init)
697 .context("Invalid OHLC bar")
698}
699
700pub fn parse_user_order_msg(
706 msg: &DeribitOrderMsg,
707 instrument: &InstrumentAny,
708 account_id: AccountId,
709 ts_init: UnixNanos,
710) -> anyhow::Result<OrderStatusReport> {
711 let instrument_id = instrument.id();
712 let venue_order_id = VenueOrderId::new(&msg.order_id);
713
714 let order_side = match msg.direction.as_str() {
715 "buy" => OrderSide::Buy,
716 "sell" => OrderSide::Sell,
717 _ => anyhow::bail!("Unknown order direction: {}", msg.direction),
718 };
719
720 let order_type = match msg.order_type.as_str() {
722 "limit" => OrderType::Limit,
723 "market" => OrderType::Market,
724 "stop_limit" => OrderType::StopLimit,
725 "stop_market" => OrderType::StopMarket,
726 "take_limit" => OrderType::LimitIfTouched,
727 "take_market" => OrderType::MarketIfTouched,
728 other => {
729 log::warn!("Unknown Deribit order_type '{other}', defaulting to Limit");
730 OrderType::Limit
731 }
732 };
733
734 let time_in_force = match msg.time_in_force.as_str() {
736 "good_til_cancelled" => TimeInForce::Gtc,
737 "good_til_day" => TimeInForce::Gtd,
738 "fill_or_kill" => TimeInForce::Fok,
739 "immediate_or_cancel" => TimeInForce::Ioc,
740 other => {
741 log::warn!("Unknown time_in_force '{other}', defaulting to GTC");
742 TimeInForce::Gtc
743 }
744 };
745
746 let order_status = match msg.order_state.as_str() {
748 "open" => {
749 if msg.filled_amount.is_zero() {
750 OrderStatus::Accepted
751 } else {
752 OrderStatus::PartiallyFilled
753 }
754 }
755 "filled" => OrderStatus::Filled,
756 "rejected" => OrderStatus::Rejected,
757 "cancelled" => OrderStatus::Canceled,
758 "untriggered" => OrderStatus::Accepted, other => {
760 log::warn!("Unknown Deribit order_state '{other}', defaulting to Accepted");
761 OrderStatus::Accepted
762 }
763 };
764
765 let price_precision = instrument.price_precision();
766 let size_precision = instrument.size_precision();
767
768 let quantity = Quantity::from_decimal_dp(msg.amount, size_precision)?;
769 let filled_qty = Quantity::from_decimal_dp(msg.filled_amount, size_precision)?;
770
771 let ts_accepted = UnixNanos::new(msg.creation_timestamp * NANOSECONDS_IN_MILLISECOND);
772 let ts_last = UnixNanos::new(msg.last_update_timestamp * NANOSECONDS_IN_MILLISECOND);
773
774 let mut report = OrderStatusReport::new(
775 account_id,
776 instrument_id,
777 None, venue_order_id,
779 order_side,
780 order_type,
781 time_in_force,
782 order_status,
783 quantity,
784 filled_qty,
785 ts_accepted,
786 ts_last,
787 ts_init,
788 Some(UUID4::new()),
789 );
790
791 if let Some(ref label) = msg.label
793 && !label.is_empty()
794 {
795 report = report.with_client_order_id(ClientOrderId::new(label));
796 }
797
798 if let Some(price_val) = msg.price
800 && !price_val.is_zero()
801 {
802 let price = Price::from_decimal_dp(price_val, price_precision)?;
803 report = report.with_price(price);
804 }
805
806 if time_in_force == TimeInForce::Gtd {
807 let expire_time = next_8_utc(ts_accepted)?;
808 report = report.with_expire_time(expire_time);
809 }
810
811 if let Some(avg_price) = msg.average_price
813 && !avg_price.is_zero()
814 {
815 report = report.with_avg_px(avg_price.to_f64().unwrap_or_default())?;
816 }
817
818 if let Some(trigger_price) = msg.trigger_price
820 && !trigger_price.is_zero()
821 {
822 let trigger = Price::from_decimal_dp(trigger_price, price_precision)?;
823 report = report.with_trigger_price(trigger);
824 }
825
826 if msg.post_only {
827 report = report.with_post_only(true);
828 }
829
830 if msg.reduce_only {
831 report = report.with_reduce_only(true);
832 }
833
834 if let Some(ref reason) = msg.reject_reason {
836 report = report.with_cancel_reason(reason.clone());
837 } else if let Some(ref reason) = msg.cancel_reason {
838 report = report.with_cancel_reason(reason.clone());
839 }
840
841 Ok(report)
842}
843
844pub fn parse_user_trade_msg(
850 msg: &DeribitUserTradeMsg,
851 instrument: &InstrumentAny,
852 account_id: AccountId,
853 ts_init: UnixNanos,
854) -> anyhow::Result<FillReport> {
855 let instrument_id = instrument.id();
856 let venue_order_id = VenueOrderId::new(&msg.order_id);
857 let trade_id = TradeId::new(&msg.trade_id);
858
859 if let Some(liq) = msg.liquidation.as_deref().filter(|s| !s.is_empty()) {
862 let who = match liq {
863 "M" => "maker",
864 "T" => "taker",
865 "MT" => "both",
866 _ => liq,
867 };
868 log::warn!(
869 "Liquidation trade: {} trade_id={} order_id={} liquidation_side={} direction={} amount={} price={}",
870 instrument_id,
871 msg.trade_id,
872 msg.order_id,
873 who,
874 msg.direction,
875 msg.amount,
876 msg.price,
877 );
878 }
879
880 let order_side = match msg.direction.as_str() {
881 "buy" => OrderSide::Buy,
882 "sell" => OrderSide::Sell,
883 _ => anyhow::bail!("Unknown trade direction: {}", msg.direction),
884 };
885
886 let price_precision = instrument.price_precision();
887 let size_precision = instrument.size_precision();
888
889 let last_qty = Quantity::from_decimal_dp(msg.amount, size_precision)?;
890 let last_px = Price::from_decimal_dp(msg.price, price_precision)?;
891
892 let liquidity_side = match msg.liquidity.as_str() {
893 "M" => LiquiditySide::Maker,
894 "T" => LiquiditySide::Taker,
895 _ => LiquiditySide::NoLiquiditySide,
896 };
897
898 let fee_currency = Currency::from(&msg.fee_currency);
900 let commission = Money::from_decimal(msg.fee, fee_currency)?;
901
902 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
903
904 let client_order_id = msg
905 .label
906 .as_ref()
907 .filter(|l| !l.is_empty())
908 .map(ClientOrderId::new);
909
910 Ok(FillReport::new(
911 account_id,
912 instrument_id,
913 venue_order_id,
914 trade_id,
915 order_side,
916 last_qty,
917 last_px,
918 commission,
919 liquidity_side,
920 client_order_id,
921 None, ts_event,
923 ts_init,
924 None, ))
926}
927
928#[must_use]
939pub fn parse_position_status_report(
940 position: &DeribitPosition,
941 instrument: &InstrumentAny,
942 account_id: AccountId,
943 ts_init: UnixNanos,
944) -> PositionStatusReport {
945 let instrument_id = instrument.id();
946 let size_precision = instrument.size_precision();
947
948 let signed_qty = Quantity::from_decimal_dp(position.size.abs(), size_precision)
949 .unwrap_or_else(|_| Quantity::new(0.0, size_precision));
950
951 let position_side = match position.direction.as_str() {
952 "buy" => PositionSideSpecified::Long,
953 "sell" => PositionSideSpecified::Short,
954 _ => PositionSideSpecified::Flat,
955 };
956
957 let avg_px_open = Some(position.average_price);
959
960 PositionStatusReport::new(
961 account_id,
962 instrument_id,
963 position_side,
964 signed_qty,
965 ts_init,
966 ts_init,
967 Some(UUID4::new()),
968 None, avg_px_open,
970 )
971}
972
973#[derive(Debug, Clone)]
978pub enum ParsedOrderEvent {
979 Accepted(OrderAccepted),
981 Canceled(OrderCanceled),
983 Expired(OrderExpired),
985 Updated(OrderUpdated),
987 None,
989}
990
991fn extract_client_order_id(msg: &DeribitOrderMsg) -> Option<ClientOrderId> {
993 msg.label
994 .as_ref()
995 .filter(|l| !l.is_empty())
996 .map(ClientOrderId::new)
997}
998
999#[must_use]
1004pub fn parse_order_accepted(
1005 msg: &DeribitOrderMsg,
1006 instrument: &InstrumentAny,
1007 account_id: AccountId,
1008 trader_id: TraderId,
1009 strategy_id: StrategyId,
1010 ts_init: UnixNanos,
1011) -> OrderAccepted {
1012 let instrument_id = instrument.id();
1013 let venue_order_id = VenueOrderId::new(&msg.order_id);
1014 let client_order_id = extract_client_order_id(msg).unwrap_or_else(|| {
1015 ClientOrderId::new(&msg.order_id)
1017 });
1018 let ts_event = UnixNanos::new(msg.last_update_timestamp * NANOSECONDS_IN_MILLISECOND);
1019
1020 OrderAccepted::new(
1021 trader_id,
1022 strategy_id,
1023 instrument_id,
1024 client_order_id,
1025 venue_order_id,
1026 account_id,
1027 nautilus_core::UUID4::new(),
1028 ts_event,
1029 ts_init,
1030 false, )
1032}
1033
1034#[must_use]
1038pub fn parse_order_canceled(
1039 msg: &DeribitOrderMsg,
1040 instrument: &InstrumentAny,
1041 account_id: AccountId,
1042 trader_id: TraderId,
1043 strategy_id: StrategyId,
1044 ts_init: UnixNanos,
1045) -> OrderCanceled {
1046 let instrument_id = instrument.id();
1047 let venue_order_id = VenueOrderId::new(&msg.order_id);
1048 let client_order_id =
1049 extract_client_order_id(msg).unwrap_or_else(|| ClientOrderId::new(&msg.order_id));
1050 let ts_event = UnixNanos::new(msg.last_update_timestamp * NANOSECONDS_IN_MILLISECOND);
1051
1052 OrderCanceled::new(
1053 trader_id,
1054 strategy_id,
1055 instrument_id,
1056 client_order_id,
1057 nautilus_core::UUID4::new(),
1058 ts_event,
1059 ts_init,
1060 false, Some(venue_order_id),
1062 Some(account_id),
1063 )
1064}
1065
1066#[must_use]
1071pub fn parse_order_expired(
1072 msg: &DeribitOrderMsg,
1073 instrument: &InstrumentAny,
1074 account_id: AccountId,
1075 trader_id: TraderId,
1076 strategy_id: StrategyId,
1077 ts_init: UnixNanos,
1078) -> OrderExpired {
1079 let instrument_id = instrument.id();
1080 let venue_order_id = VenueOrderId::new(&msg.order_id);
1081 let client_order_id =
1082 extract_client_order_id(msg).unwrap_or_else(|| ClientOrderId::new(&msg.order_id));
1083 let ts_event = UnixNanos::new(msg.last_update_timestamp * NANOSECONDS_IN_MILLISECOND);
1084
1085 OrderExpired::new(
1086 trader_id,
1087 strategy_id,
1088 instrument_id,
1089 client_order_id,
1090 nautilus_core::UUID4::new(),
1091 ts_event,
1092 ts_init,
1093 false, Some(venue_order_id),
1095 Some(account_id),
1096 )
1097}
1098
1099#[must_use]
1103pub fn parse_order_updated(
1104 msg: &DeribitOrderMsg,
1105 instrument: &InstrumentAny,
1106 account_id: AccountId,
1107 trader_id: TraderId,
1108 strategy_id: StrategyId,
1109 ts_init: UnixNanos,
1110) -> OrderUpdated {
1111 let instrument_id = instrument.id();
1112 let price_precision = instrument.price_precision();
1113 let size_precision = instrument.size_precision();
1114
1115 let venue_order_id = VenueOrderId::new(&msg.order_id);
1116 let client_order_id =
1117 extract_client_order_id(msg).unwrap_or_else(|| ClientOrderId::new(&msg.order_id));
1118 let quantity = Quantity::from_decimal_dp(msg.amount, size_precision)
1119 .unwrap_or_else(|_| Quantity::new(0.0, size_precision));
1120 let price = msg
1121 .price
1122 .and_then(|p| Price::from_decimal_dp(p, price_precision).ok());
1123 let trigger_price = msg
1124 .trigger_price
1125 .and_then(|p| Price::from_decimal_dp(p, price_precision).ok());
1126 let ts_event = UnixNanos::new(msg.last_update_timestamp * NANOSECONDS_IN_MILLISECOND);
1127
1128 OrderUpdated::new(
1129 trader_id,
1130 strategy_id,
1131 instrument_id,
1132 client_order_id,
1133 quantity,
1134 nautilus_core::UUID4::new(),
1135 ts_event,
1136 ts_init,
1137 false, Some(venue_order_id),
1139 Some(account_id),
1140 price,
1141 trigger_price,
1142 None, false, )
1145}
1146
1147#[must_use]
1160pub fn determine_order_event_type(
1161 order_state: &str,
1162 is_new_order: bool,
1163 was_amended: bool,
1164) -> OrderEventType {
1165 match order_state {
1166 "open" | "untriggered" => {
1167 if was_amended {
1168 OrderEventType::Updated
1169 } else if is_new_order {
1170 OrderEventType::Accepted
1171 } else {
1172 OrderEventType::None
1174 }
1175 }
1176 "cancelled" => OrderEventType::Canceled,
1177 "expired" => OrderEventType::Expired,
1178 "filled" => {
1179 OrderEventType::None
1181 }
1182 "rejected" => {
1183 OrderEventType::None
1185 }
1186 other => {
1187 log::warn!("Unknown Deribit order_state '{other}' in event routing, dropping");
1188 OrderEventType::None
1189 }
1190 }
1191}
1192
1193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1195pub enum OrderEventType {
1196 Accepted,
1198 Canceled,
1200 Expired,
1202 Updated,
1204 None,
1206}
1207
1208#[cfg(test)]
1209mod tests {
1210 use rstest::rstest;
1211 use rust_decimal_macros::dec;
1212
1213 use super::*;
1214 use crate::{
1215 common::{parse::parse_deribit_instrument_any, testing::load_test_json},
1216 http::models::{DeribitInstrument, DeribitJsonRpcResponse},
1217 };
1218
1219 fn test_perpetual_instrument() -> InstrumentAny {
1221 let json = load_test_json("http_get_instruments.json");
1222 let response: DeribitJsonRpcResponse<Vec<DeribitInstrument>> =
1223 serde_json::from_str(&json).unwrap();
1224 let instrument = &response.result.unwrap()[0];
1225 parse_deribit_instrument_any(instrument, UnixNanos::default(), UnixNanos::default())
1226 .unwrap()
1227 .unwrap()
1228 }
1229
1230 #[rstest]
1231 fn test_parse_trade_msg_sell() {
1232 let instrument = test_perpetual_instrument();
1233 let json = load_test_json("ws_trades.json");
1234 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1235 let trades: Vec<DeribitTradeMsg> =
1236 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1237 let msg = &trades[0];
1238
1239 let tick = parse_trade_msg(msg, &instrument, UnixNanos::default()).unwrap();
1240
1241 assert_eq!(tick.instrument_id, instrument.id());
1242 assert_eq!(tick.price, instrument.make_price(92294.5));
1243 assert_eq!(tick.size, instrument.make_qty(10.0, None));
1244 assert_eq!(tick.aggressor_side, AggressorSide::Seller);
1245 assert_eq!(tick.trade_id.to_string(), "403691824");
1246 assert_eq!(tick.ts_event, UnixNanos::new(1_765_531_356_452_000_000));
1247 }
1248
1249 #[rstest]
1250 fn test_parse_trade_msg_buy() {
1251 let instrument = test_perpetual_instrument();
1252 let json = load_test_json("ws_trades.json");
1253 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1254 let trades: Vec<DeribitTradeMsg> =
1255 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1256 let msg = &trades[1];
1257
1258 let tick = parse_trade_msg(msg, &instrument, UnixNanos::default()).unwrap();
1259
1260 assert_eq!(tick.instrument_id, instrument.id());
1261 assert_eq!(tick.price, instrument.make_price(92288.5));
1262 assert_eq!(tick.size, instrument.make_qty(750.0, None));
1263 assert_eq!(tick.aggressor_side, AggressorSide::Seller);
1264 assert_eq!(tick.trade_id.to_string(), "403691825");
1265 }
1266
1267 #[rstest]
1268 fn test_parse_book_snapshot() {
1269 let instrument = test_perpetual_instrument();
1270 let json = load_test_json("ws_book_snapshot.json");
1271 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1272 let msg: DeribitBookMsg =
1273 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1274
1275 let deltas = parse_book_snapshot(&msg, &instrument, UnixNanos::default()).unwrap();
1276
1277 assert_eq!(deltas.instrument_id, instrument.id());
1278 assert_eq!(deltas.deltas.len(), 11);
1280
1281 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
1283
1284 let first_bid = &deltas.deltas[1];
1286 assert_eq!(first_bid.action, BookAction::Add);
1287 assert_eq!(first_bid.order.side, OrderSide::Buy);
1288 assert_eq!(first_bid.order.price, instrument.make_price(42500.0));
1289 assert_eq!(first_bid.order.size, instrument.make_qty(1000.0, None));
1290
1291 let first_ask = &deltas.deltas[6];
1293 assert_eq!(first_ask.action, BookAction::Add);
1294 assert_eq!(first_ask.order.side, OrderSide::Sell);
1295 assert_eq!(first_ask.order.price, instrument.make_price(42501.0));
1296 assert_eq!(first_ask.order.size, instrument.make_qty(800.0, None));
1297
1298 let last = deltas.deltas.last().unwrap();
1300 assert_eq!(
1301 last.flags & RecordFlag::F_LAST as u8,
1302 RecordFlag::F_LAST as u8
1303 );
1304 }
1305
1306 #[rstest]
1307 fn test_parse_book_delta() {
1308 let instrument = test_perpetual_instrument();
1309 let json = load_test_json("ws_book_delta.json");
1310 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1311 let msg: DeribitBookMsg =
1312 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1313
1314 let deltas = parse_book_delta(&msg, &instrument, UnixNanos::default()).unwrap();
1315
1316 assert_eq!(deltas.instrument_id, instrument.id());
1317 assert_eq!(deltas.deltas.len(), 4);
1319
1320 let bid_change = &deltas.deltas[0];
1322 assert_eq!(bid_change.action, BookAction::Update);
1323 assert_eq!(bid_change.order.side, OrderSide::Buy);
1324 assert_eq!(bid_change.order.price, instrument.make_price(42500.0));
1325 assert_eq!(bid_change.order.size, instrument.make_qty(950.0, None));
1326
1327 let bid_new = &deltas.deltas[1];
1329 assert_eq!(bid_new.action, BookAction::Add);
1330 assert_eq!(bid_new.order.side, OrderSide::Buy);
1331 assert_eq!(bid_new.order.price, instrument.make_price(42498.5));
1332 assert_eq!(bid_new.order.size, instrument.make_qty(300.0, None));
1333
1334 let ask_delete = &deltas.deltas[2];
1336 assert_eq!(ask_delete.action, BookAction::Delete);
1337 assert_eq!(ask_delete.order.side, OrderSide::Sell);
1338 assert_eq!(ask_delete.order.price, instrument.make_price(42501.0));
1339 assert_eq!(ask_delete.order.size, instrument.make_qty(0.0, None));
1340
1341 let ask_change = &deltas.deltas[3];
1343 assert_eq!(ask_change.action, BookAction::Update);
1344 assert_eq!(ask_change.order.side, OrderSide::Sell);
1345 assert_eq!(ask_change.order.price, instrument.make_price(42501.5));
1346 assert_eq!(ask_change.order.size, instrument.make_qty(700.0, None));
1347
1348 let last = deltas.deltas.last().unwrap();
1350 assert_eq!(
1351 last.flags & RecordFlag::F_LAST as u8,
1352 RecordFlag::F_LAST as u8
1353 );
1354 }
1355
1356 #[rstest]
1357 fn test_parse_ticker_to_quote() {
1358 let instrument = test_perpetual_instrument();
1359 let json = load_test_json("ws_ticker.json");
1360 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1361 let msg: DeribitTickerMsg =
1362 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1363
1364 assert_eq!(msg.instrument_name.as_str(), "BTC-PERPETUAL");
1366 assert_eq!(msg.timestamp, 1_765_541_474_086);
1367 assert_eq!(msg.best_bid_price, Some(dec!(92283.5)));
1368 assert_eq!(msg.best_ask_price, Some(dec!(92284.0)));
1369 assert_eq!(msg.best_bid_amount, Some(dec!(117660.0)));
1370 assert_eq!(msg.best_ask_amount, Some(dec!(186520.0)));
1371 assert_eq!(msg.mark_price, dec!(92281.78));
1372 assert_eq!(msg.index_price, dec!(92263.55));
1373 assert_eq!(msg.open_interest, dec!(1132329370.0));
1374
1375 let quote = parse_ticker_to_quote(&msg, &instrument, UnixNanos::default()).unwrap();
1376
1377 assert_eq!(quote.instrument_id, instrument.id());
1378 assert_eq!(quote.bid_price, instrument.make_price(92283.5));
1379 assert_eq!(quote.ask_price, instrument.make_price(92284.0));
1380 assert_eq!(quote.bid_size, instrument.make_qty(117660.0, None));
1381 assert_eq!(quote.ask_size, instrument.make_qty(186520.0, None));
1382 assert_eq!(quote.ts_event, UnixNanos::new(1_765_541_474_086_000_000));
1383 }
1384
1385 #[rstest]
1386 fn test_parse_quote_msg() {
1387 let instrument = test_perpetual_instrument();
1388 let json = load_test_json("ws_quote.json");
1389 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1390 let msg: DeribitQuoteMsg =
1391 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1392
1393 assert_eq!(msg.instrument_name.as_str(), "BTC-PERPETUAL");
1395 assert_eq!(msg.timestamp, 1_765_541_767_174);
1396 assert_eq!(msg.best_bid_price, dec!(92288.0));
1397 assert_eq!(msg.best_ask_price, dec!(92288.5));
1398 assert_eq!(msg.best_bid_amount, dec!(133440.0));
1399 assert_eq!(msg.best_ask_amount, dec!(99470.0));
1400
1401 let quote = parse_quote_msg(&msg, &instrument, UnixNanos::default()).unwrap();
1402
1403 assert_eq!(quote.instrument_id, instrument.id());
1404 assert_eq!(quote.bid_price, instrument.make_price(92288.0));
1405 assert_eq!(quote.ask_price, instrument.make_price(92288.5));
1406 assert_eq!(quote.bid_size, instrument.make_qty(133440.0, None));
1407 assert_eq!(quote.ask_size, instrument.make_qty(99470.0, None));
1408 assert_eq!(quote.ts_event, UnixNanos::new(1_765_541_767_174_000_000));
1409 }
1410
1411 #[rstest]
1412 fn test_parse_book_msg_snapshot() {
1413 let instrument = test_perpetual_instrument();
1414 let json = load_test_json("ws_book_snapshot.json");
1415 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1416 let msg: DeribitBookMsg =
1417 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1418
1419 assert_eq!(
1421 msg.bids[0].len(),
1422 3,
1423 "Snapshot bids should have 3 elements: [action, price, amount]"
1424 );
1425 assert_eq!(
1426 msg.bids[0][0].as_str(),
1427 Some("new"),
1428 "First element should be 'new' action for snapshot"
1429 );
1430 assert_eq!(
1431 msg.asks[0].len(),
1432 3,
1433 "Snapshot asks should have 3 elements: [action, price, amount]"
1434 );
1435 assert_eq!(
1436 msg.asks[0][0].as_str(),
1437 Some("new"),
1438 "First element should be 'new' action for snapshot"
1439 );
1440
1441 let deltas = parse_book_msg(&msg, &instrument, UnixNanos::default()).unwrap();
1442
1443 assert_eq!(deltas.instrument_id, instrument.id());
1444 assert_eq!(deltas.deltas.len(), 11);
1446
1447 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
1449
1450 let first_bid = &deltas.deltas[1];
1452 assert_eq!(first_bid.action, BookAction::Add);
1453 assert_eq!(first_bid.order.side, OrderSide::Buy);
1454 assert_eq!(first_bid.order.price, instrument.make_price(42500.0));
1455 assert_eq!(first_bid.order.size, instrument.make_qty(1000.0, None));
1456
1457 let first_ask = &deltas.deltas[6];
1459 assert_eq!(first_ask.action, BookAction::Add);
1460 assert_eq!(first_ask.order.side, OrderSide::Sell);
1461 assert_eq!(first_ask.order.price, instrument.make_price(42501.0));
1462 assert_eq!(first_ask.order.size, instrument.make_qty(800.0, None));
1463 }
1464
1465 #[rstest]
1466 fn test_parse_book_msg_delta() {
1467 let instrument = test_perpetual_instrument();
1468 let json = load_test_json("ws_book_delta.json");
1469 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1470 let msg: DeribitBookMsg =
1471 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1472
1473 assert_eq!(
1475 msg.bids[0].len(),
1476 3,
1477 "Delta bids should have 3 elements: [action, price, amount]"
1478 );
1479 assert_eq!(
1480 msg.bids[0][0].as_str(),
1481 Some("change"),
1482 "First bid should be 'change' action"
1483 );
1484 assert_eq!(
1485 msg.bids[1][0].as_str(),
1486 Some("new"),
1487 "Second bid should be 'new' action"
1488 );
1489 assert_eq!(
1490 msg.asks[0].len(),
1491 3,
1492 "Delta asks should have 3 elements: [action, price, amount]"
1493 );
1494 assert_eq!(
1495 msg.asks[0][0].as_str(),
1496 Some("delete"),
1497 "First ask should be 'delete' action"
1498 );
1499
1500 let deltas = parse_book_msg(&msg, &instrument, UnixNanos::default()).unwrap();
1501
1502 assert_eq!(deltas.instrument_id, instrument.id());
1503 assert_eq!(deltas.deltas.len(), 4);
1505
1506 assert_ne!(deltas.deltas[0].action, BookAction::Clear);
1508
1509 let bid_change = &deltas.deltas[0];
1511 assert_eq!(bid_change.action, BookAction::Update);
1512 assert_eq!(bid_change.order.side, OrderSide::Buy);
1513 assert_eq!(bid_change.order.price, instrument.make_price(42500.0));
1514 assert_eq!(bid_change.order.size, instrument.make_qty(950.0, None));
1515
1516 let bid_new = &deltas.deltas[1];
1518 assert_eq!(bid_new.action, BookAction::Add);
1519 assert_eq!(bid_new.order.side, OrderSide::Buy);
1520 assert_eq!(bid_new.order.price, instrument.make_price(42498.5));
1521 assert_eq!(bid_new.order.size, instrument.make_qty(300.0, None));
1522
1523 let ask_delete = &deltas.deltas[2];
1525 assert_eq!(ask_delete.action, BookAction::Delete);
1526 assert_eq!(ask_delete.order.side, OrderSide::Sell);
1527 assert_eq!(ask_delete.order.price, instrument.make_price(42501.0));
1528
1529 let ask_change = &deltas.deltas[3];
1531 assert_eq!(ask_change.action, BookAction::Update);
1532 assert_eq!(ask_change.order.side, OrderSide::Sell);
1533 assert_eq!(ask_change.order.price, instrument.make_price(42501.5));
1534 assert_eq!(ask_change.order.size, instrument.make_qty(700.0, None));
1535 }
1536
1537 #[rstest]
1538 fn test_parse_book_grouped_snapshot() {
1539 let instrument = test_perpetual_instrument();
1542 let json = load_test_json("ws_book_grouped_snapshot.json");
1543 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1544 let msg: DeribitBookMsg =
1545 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1546
1547 assert_eq!(
1549 msg.bids[0].len(),
1550 2,
1551 "Grouped bids should have 2 elements: [price, amount]"
1552 );
1553 assert_eq!(
1554 msg.asks[0].len(),
1555 2,
1556 "Grouped asks should have 2 elements: [price, amount]"
1557 );
1558
1559 assert_eq!(
1561 msg.msg_type,
1562 DeribitBookMsgType::Snapshot,
1563 "Grouped channel should default to Snapshot type"
1564 );
1565
1566 let deltas = parse_book_snapshot(&msg, &instrument, UnixNanos::default()).unwrap();
1567
1568 assert_eq!(deltas.instrument_id, instrument.id());
1569 assert_eq!(deltas.deltas.len(), 21);
1571
1572 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
1574
1575 let first_bid = &deltas.deltas[1];
1577 assert_eq!(first_bid.action, BookAction::Add);
1578 assert_eq!(first_bid.order.side, OrderSide::Buy);
1579 assert_eq!(first_bid.order.price, instrument.make_price(89532.5));
1580 assert_eq!(first_bid.order.size, instrument.make_qty(254900.0, None));
1581
1582 let first_ask = &deltas.deltas[11];
1584 assert_eq!(first_ask.action, BookAction::Add);
1585 assert_eq!(first_ask.order.side, OrderSide::Sell);
1586 assert_eq!(first_ask.order.price, instrument.make_price(89533.0));
1587 assert_eq!(first_ask.order.size, instrument.make_qty(91570.0, None));
1588
1589 let last = deltas.deltas.last().unwrap();
1591 assert_eq!(
1592 last.flags & RecordFlag::F_LAST as u8,
1593 RecordFlag::F_LAST as u8
1594 );
1595 }
1596
1597 #[rstest]
1598 fn test_parse_ticker_to_mark_price() {
1599 let instrument = test_perpetual_instrument();
1600 let json = load_test_json("ws_ticker.json");
1601 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1602 let msg: DeribitTickerMsg =
1603 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1604
1605 let mark_price =
1606 parse_ticker_to_mark_price(&msg, &instrument, UnixNanos::default()).unwrap();
1607
1608 assert_eq!(mark_price.instrument_id, instrument.id());
1609 assert_eq!(mark_price.value, instrument.make_price(92281.78));
1610 assert_eq!(
1611 mark_price.ts_event,
1612 UnixNanos::new(1_765_541_474_086_000_000)
1613 );
1614 }
1615
1616 #[rstest]
1617 fn test_parse_ticker_to_index_price() {
1618 let instrument = test_perpetual_instrument();
1619 let json = load_test_json("ws_ticker.json");
1620 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1621 let msg: DeribitTickerMsg =
1622 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1623
1624 let index_price =
1625 parse_ticker_to_index_price(&msg, &instrument, UnixNanos::default()).unwrap();
1626
1627 assert_eq!(index_price.instrument_id, instrument.id());
1628 assert_eq!(index_price.value, instrument.make_price(92263.55));
1629 assert_eq!(
1630 index_price.ts_event,
1631 UnixNanos::new(1_765_541_474_086_000_000)
1632 );
1633 }
1634
1635 #[rstest]
1636 fn test_parse_ticker_to_funding_rate() {
1637 let instrument = test_perpetual_instrument();
1638 let json = load_test_json("ws_ticker.json");
1639 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1640 let msg: DeribitTickerMsg =
1641 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1642
1643 assert!(msg.current_funding.is_some());
1645
1646 let funding_rate =
1647 parse_ticker_to_funding_rate(&msg, &instrument, UnixNanos::default()).unwrap();
1648
1649 assert_eq!(funding_rate.instrument_id, instrument.id());
1650 assert_eq!(
1652 funding_rate.ts_event,
1653 UnixNanos::new(1_765_541_474_086_000_000)
1654 );
1655 assert!(funding_rate.interval.is_none());
1656 assert!(funding_rate.next_funding_ns.is_none()); }
1658
1659 #[rstest]
1660 fn test_resolution_to_bar_type_1_minute() {
1661 let instrument_id = InstrumentId::from("BTC-PERPETUAL.DERIBIT");
1662 let bar_type = resolution_to_bar_type(instrument_id, "1").unwrap();
1663
1664 assert_eq!(bar_type.instrument_id(), instrument_id);
1665 assert_eq!(bar_type.spec().step.get(), 1);
1666 assert_eq!(bar_type.spec().aggregation, BarAggregation::Minute);
1667 assert_eq!(bar_type.spec().price_type, PriceType::Last);
1668 assert_eq!(bar_type.aggregation_source(), AggregationSource::External);
1669 }
1670
1671 #[rstest]
1672 fn test_resolution_to_bar_type_60_minute() {
1673 let instrument_id = InstrumentId::from("ETH-PERPETUAL.DERIBIT");
1674 let bar_type = resolution_to_bar_type(instrument_id, "60").unwrap();
1675
1676 assert_eq!(bar_type.instrument_id(), instrument_id);
1677 assert_eq!(bar_type.spec().step.get(), 60);
1678 assert_eq!(bar_type.spec().aggregation, BarAggregation::Minute);
1679 }
1680
1681 #[rstest]
1682 fn test_resolution_to_bar_type_daily() {
1683 let instrument_id = InstrumentId::from("BTC-PERPETUAL.DERIBIT");
1684 let bar_type = resolution_to_bar_type(instrument_id, "1D").unwrap();
1685
1686 assert_eq!(bar_type.instrument_id(), instrument_id);
1687 assert_eq!(bar_type.spec().step.get(), 1);
1688 assert_eq!(bar_type.spec().aggregation, BarAggregation::Day);
1689 }
1690
1691 #[rstest]
1692 fn test_resolution_to_bar_type_invalid() {
1693 let instrument_id = InstrumentId::from("BTC-PERPETUAL.DERIBIT");
1694 let result = resolution_to_bar_type(instrument_id, "invalid");
1695
1696 assert!(result.is_err());
1697 assert!(
1698 result
1699 .unwrap_err()
1700 .to_string()
1701 .contains("Unsupported Deribit resolution")
1702 );
1703 }
1704
1705 #[rstest]
1706 fn test_parse_chart_msg() {
1707 let instrument = test_perpetual_instrument();
1708 let json = load_test_json("ws_chart.json");
1709 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1710 let chart_msg: DeribitChartMsg =
1711 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1712
1713 assert_eq!(chart_msg.tick, 1_767_200_040_000);
1715 assert_eq!(chart_msg.open, 87490.0);
1716 assert_eq!(chart_msg.high, 87500.0);
1717 assert_eq!(chart_msg.low, 87465.0);
1718 assert_eq!(chart_msg.close, 87474.0);
1719 assert_eq!(chart_msg.volume, 0.95978896);
1720 assert_eq!(chart_msg.cost, 83970.0);
1721
1722 let bar_type = resolution_to_bar_type(instrument.id(), "1").unwrap();
1723
1724 let bar = parse_chart_msg(
1726 &chart_msg,
1727 bar_type,
1728 instrument.price_precision(),
1729 instrument.size_precision(),
1730 true,
1731 UnixNanos::default(),
1732 )
1733 .unwrap();
1734
1735 assert_eq!(bar.bar_type, bar_type);
1736 assert_eq!(bar.open, instrument.make_price(87490.0));
1737 assert_eq!(bar.high, instrument.make_price(87500.0));
1738 assert_eq!(bar.low, instrument.make_price(87465.0));
1739 assert_eq!(bar.close, instrument.make_price(87474.0));
1740 assert_eq!(bar.volume, instrument.make_qty(1.0, None)); assert_eq!(bar.ts_event, UnixNanos::new(1_767_200_100_000_000_000));
1744 }
1745
1746 #[rstest]
1747 fn test_parse_order_buy_response() {
1748 let instrument = test_perpetual_instrument();
1749 let json = load_test_json("ws_order_buy_response.json");
1750 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1751
1752 let order_msg: DeribitOrderMsg =
1754 serde_json::from_value(response["result"]["order"].clone()).unwrap();
1755
1756 assert_eq!(order_msg.order_id, "USDC-104819327443");
1758 assert_eq!(
1759 order_msg.label,
1760 Some("O-19700101-000000-001-001-1".to_string())
1761 );
1762 assert_eq!(order_msg.direction, "buy");
1763 assert_eq!(order_msg.order_state, "open");
1764 assert_eq!(order_msg.order_type, "limit");
1765 assert_eq!(order_msg.price, Some(dec!(2973.55)));
1766 assert_eq!(order_msg.amount, dec!(0.001));
1767 assert_eq!(order_msg.filled_amount, rust_decimal::Decimal::ZERO);
1768 assert!(order_msg.post_only);
1769 assert!(!order_msg.reduce_only);
1770
1771 let account_id = AccountId::new("DERIBIT-001");
1773 let trader_id = TraderId::new("TRADER-001");
1774 let strategy_id = StrategyId::new("PMM-001");
1775
1776 let accepted = parse_order_accepted(
1777 &order_msg,
1778 &instrument,
1779 account_id,
1780 trader_id,
1781 strategy_id,
1782 UnixNanos::default(),
1783 );
1784
1785 assert_eq!(
1786 accepted.client_order_id.to_string(),
1787 "O-19700101-000000-001-001-1"
1788 );
1789 assert_eq!(accepted.venue_order_id.to_string(), "USDC-104819327443");
1790 assert_eq!(accepted.trader_id, trader_id);
1791 assert_eq!(accepted.strategy_id, strategy_id);
1792 assert_eq!(accepted.account_id, account_id);
1793 }
1794
1795 #[rstest]
1796 fn test_parse_order_sell_response() {
1797 let instrument = test_perpetual_instrument();
1798 let json = load_test_json("ws_order_sell_response.json");
1799 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1800
1801 let order_msg: DeribitOrderMsg =
1802 serde_json::from_value(response["result"]["order"].clone()).unwrap();
1803
1804 assert_eq!(order_msg.order_id, "USDC-104819327458");
1806 assert_eq!(
1807 order_msg.label,
1808 Some("O-19700101-000000-001-001-2".to_string())
1809 );
1810 assert_eq!(order_msg.direction, "sell");
1811 assert_eq!(order_msg.order_state, "open");
1812 assert_eq!(order_msg.price, Some(dec!(3286.7)));
1813 assert_eq!(order_msg.amount, dec!(0.001));
1814
1815 let account_id = AccountId::new("DERIBIT-001");
1817 let trader_id = TraderId::new("TRADER-001");
1818 let strategy_id = StrategyId::new("PMM-001");
1819
1820 let accepted = parse_order_accepted(
1821 &order_msg,
1822 &instrument,
1823 account_id,
1824 trader_id,
1825 strategy_id,
1826 UnixNanos::default(),
1827 );
1828
1829 assert_eq!(
1830 accepted.client_order_id.to_string(),
1831 "O-19700101-000000-001-001-2"
1832 );
1833 assert_eq!(accepted.venue_order_id.to_string(), "USDC-104819327458");
1834 assert_eq!(accepted.trader_id, trader_id);
1835 assert_eq!(accepted.strategy_id, strategy_id);
1836 assert_eq!(accepted.account_id, account_id);
1837 }
1838
1839 #[rstest]
1840 fn test_parse_order_edit_response() {
1841 let instrument = test_perpetual_instrument();
1842 let json = load_test_json("ws_order_edit_response.json");
1843 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1844
1845 let order_msg: DeribitOrderMsg =
1846 serde_json::from_value(response["result"]["order"].clone()).unwrap();
1847
1848 assert_eq!(order_msg.order_id, "USDC-104819327443");
1850 assert_eq!(
1851 order_msg.label,
1852 Some("O-19700101-000000-001-001-1".to_string())
1853 );
1854 assert_eq!(order_msg.direction, "buy");
1855 assert_eq!(order_msg.order_state, "open");
1856 assert_eq!(order_msg.price, Some(dec!(3067.2))); let account_id = AccountId::new("DERIBIT-001");
1860 let trader_id = TraderId::new("TRADER-001");
1861 let strategy_id = StrategyId::new("PMM-001");
1862
1863 let updated = parse_order_updated(
1864 &order_msg,
1865 &instrument,
1866 account_id,
1867 trader_id,
1868 strategy_id,
1869 UnixNanos::default(),
1870 );
1871
1872 assert_eq!(
1873 updated.client_order_id.to_string(),
1874 "O-19700101-000000-001-001-1"
1875 );
1876 assert_eq!(
1877 updated.venue_order_id.unwrap().to_string(),
1878 "USDC-104819327443"
1879 );
1880 assert_eq!(updated.quantity.as_f64(), 0.0);
1882 }
1883
1884 #[rstest]
1885 fn test_parse_order_cancel_response() {
1886 let instrument = test_perpetual_instrument();
1887 let json = load_test_json("ws_order_cancel_response.json");
1888 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1889
1890 let order_msg: DeribitOrderMsg =
1892 serde_json::from_value(response["result"].clone()).unwrap();
1893
1894 assert_eq!(order_msg.order_id, "USDC-104819327443");
1896 assert_eq!(
1897 order_msg.label,
1898 Some("O-19700101-000000-001-001-1".to_string())
1899 );
1900 assert_eq!(order_msg.order_state, "cancelled");
1901 assert_eq!(order_msg.cancel_reason, Some("user_request".to_string()));
1902
1903 let account_id = AccountId::new("DERIBIT-001");
1905 let trader_id = TraderId::new("TRADER-001");
1906 let strategy_id = StrategyId::new("PMM-001");
1907
1908 let canceled = parse_order_canceled(
1909 &order_msg,
1910 &instrument,
1911 account_id,
1912 trader_id,
1913 strategy_id,
1914 UnixNanos::default(),
1915 );
1916
1917 assert_eq!(
1918 canceled.client_order_id.to_string(),
1919 "O-19700101-000000-001-001-1"
1920 );
1921 assert_eq!(
1922 canceled.venue_order_id.unwrap().to_string(),
1923 "USDC-104819327443"
1924 );
1925 assert_eq!(canceled.trader_id, trader_id);
1926 assert_eq!(canceled.strategy_id, strategy_id);
1927 }
1928
1929 #[rstest]
1930 fn test_parse_order_stop_market_response() {
1931 let instrument = test_perpetual_instrument();
1936 let json = load_test_json("ws_order_stop_market_response.json");
1937 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1938
1939 let order_msg: DeribitOrderMsg =
1940 serde_json::from_value(response["result"]["order"].clone()).unwrap();
1941
1942 assert_eq!(order_msg.order_id, "USDC-104819327499");
1943 assert_eq!(order_msg.order_type, "stop_market");
1944 assert_eq!(order_msg.order_state, "untriggered");
1945 assert_eq!(order_msg.price, None);
1946 assert_eq!(order_msg.trigger_price, Some(dec!(2228.0)));
1947 assert_eq!(order_msg.trigger.as_deref(), Some("mark_price"));
1948 assert!(order_msg.reduce_only);
1949
1950 let account_id = AccountId::new("DERIBIT-001");
1951 let report =
1952 parse_user_order_msg(&order_msg, &instrument, account_id, UnixNanos::default())
1953 .unwrap();
1954
1955 assert_eq!(report.order_type, OrderType::StopMarket);
1956 assert_eq!(report.order_status, OrderStatus::Accepted);
1957 assert!(report.price.is_none());
1958 assert!(report.trigger_price.is_some());
1959 assert!(report.reduce_only);
1960 }
1961
1962 #[rstest]
1963 fn test_parse_user_order_msg_to_status_report() {
1964 let instrument = test_perpetual_instrument();
1965 let json = load_test_json("ws_order_buy_response.json");
1966 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1967
1968 let order_msg: DeribitOrderMsg =
1969 serde_json::from_value(response["result"]["order"].clone()).unwrap();
1970
1971 let account_id = AccountId::new("DERIBIT-001");
1972 let report =
1973 parse_user_order_msg(&order_msg, &instrument, account_id, UnixNanos::default())
1974 .unwrap();
1975
1976 assert_eq!(report.venue_order_id.to_string(), "USDC-104819327443");
1977 assert_eq!(
1978 report.client_order_id.unwrap().to_string(),
1979 "O-19700101-000000-001-001-1"
1980 );
1981 assert_eq!(report.order_side, OrderSide::Buy);
1982 assert_eq!(report.order_type, OrderType::Limit);
1983 assert_eq!(report.time_in_force, TimeInForce::Gtc);
1984 assert_eq!(report.order_status, OrderStatus::Accepted);
1985 assert_eq!(report.quantity.as_f64(), 0.0);
1987 assert_eq!(report.filled_qty.as_f64(), 0.0);
1988 assert!(report.post_only);
1989 assert!(!report.reduce_only);
1990 }
1991
1992 #[rstest]
1993 fn test_determine_order_event_type() {
1994 assert_eq!(
1996 determine_order_event_type("open", true, false),
1997 OrderEventType::Accepted
1998 );
1999
2000 assert_eq!(
2002 determine_order_event_type("open", false, true),
2003 OrderEventType::Updated
2004 );
2005
2006 assert_eq!(
2008 determine_order_event_type("cancelled", false, false),
2009 OrderEventType::Canceled
2010 );
2011
2012 assert_eq!(
2014 determine_order_event_type("expired", false, false),
2015 OrderEventType::Expired
2016 );
2017
2018 assert_eq!(
2020 determine_order_event_type("filled", false, false),
2021 OrderEventType::None
2022 );
2023 }
2024}