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 },
27 enums::{
28 AggregationSource, AggressorSide, BarAggregation, BookAction, LiquiditySide, OrderSide,
29 OrderStatus, OrderType, PositionSideSpecified, PriceType, RecordFlag, TimeInForce,
30 },
31 events::{OrderAccepted, OrderCanceled, OrderExpired, OrderUpdated},
32 identifiers::{
33 AccountId, ClientOrderId, InstrumentId, StrategyId, TradeId, TraderId, VenueOrderId,
34 },
35 instruments::{Instrument, InstrumentAny},
36 reports::{FillReport, OrderStatusReport, PositionStatusReport},
37 types::{Currency, Money, Price, Quantity},
38};
39use rust_decimal::prelude::ToPrimitive;
40use ustr::Ustr;
41
42use super::{
43 enums::{DeribitBookAction, DeribitBookMsgType},
44 messages::{
45 DeribitBookMsg, DeribitChartMsg, DeribitOrderMsg, DeribitPerpetualMsg, DeribitQuoteMsg,
46 DeribitTickerMsg, DeribitTradeMsg, DeribitUserTradeMsg,
47 },
48};
49use crate::http::models::DeribitPosition;
50
51fn next_8_utc(from_ns: UnixNanos) -> UnixNanos {
52 let from_secs = from_ns.as_u64() / 1_000_000_000;
53 let dt = Utc.timestamp_opt(from_secs as i64, 0).unwrap();
54 let next_8 = if dt.hour() < 8 {
55 dt.date_naive().and_hms_opt(8, 0, 0).unwrap().and_utc()
56 } else {
57 (dt.date_naive() + Duration::days(1))
58 .and_hms_opt(8, 0, 0)
59 .unwrap()
60 .and_utc()
61 };
62 UnixNanos::from(next_8.timestamp_nanos_opt().unwrap() as u64)
63}
64
65pub fn parse_trade_msg(
71 msg: &DeribitTradeMsg,
72 instrument: &InstrumentAny,
73 ts_init: UnixNanos,
74) -> anyhow::Result<TradeTick> {
75 let instrument_id = instrument.id();
76 let price_precision = instrument.price_precision();
77 let size_precision = instrument.size_precision();
78
79 let price = Price::from_decimal_dp(msg.price, price_precision)?;
80 let size = Quantity::from_decimal_dp(msg.amount.abs(), size_precision)?;
81
82 let aggressor_side = match msg.direction.as_str() {
83 "buy" => AggressorSide::Buyer,
84 "sell" => AggressorSide::Seller,
85 _ => AggressorSide::NoAggressor,
86 };
87
88 let trade_id = TradeId::new(&msg.trade_id);
89 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
90
91 TradeTick::new_checked(
92 instrument_id,
93 price,
94 size,
95 aggressor_side,
96 trade_id,
97 ts_event,
98 ts_init,
99 )
100}
101
102pub fn parse_trades_data(
104 trades: Vec<DeribitTradeMsg>,
105 instruments_cache: &AHashMap<Ustr, InstrumentAny>,
106 ts_init: UnixNanos,
107) -> Vec<Data> {
108 trades
109 .iter()
110 .filter_map(|msg| {
111 instruments_cache
112 .get(&msg.instrument_name)
113 .and_then(|inst| parse_trade_msg(msg, inst, ts_init).ok())
114 .map(Data::Trade)
115 })
116 .collect()
117}
118
119fn parse_snapshot_level(
120 level: &[serde_json::Value],
121 index: usize,
122 side: &str,
123 instrument_name: &str,
124) -> Option<(f64, f64)> {
125 let (price_val, amount_val) = if level.len() >= 3 {
126 let price = level[1].as_f64().or_else(|| {
127 log::warn!(
128 "Failed to parse {side} price at index {index} for {instrument_name}: {level:?}"
129 );
130 None
131 })?;
132 let amount = level[2].as_f64().or_else(|| {
133 log::warn!(
134 "Failed to parse {side} amount at index {index} for {instrument_name}: {level:?}"
135 );
136 None
137 })?;
138 (price, amount)
139 } else if level.len() >= 2 {
140 let price = level[0].as_f64().or_else(|| {
141 log::warn!(
142 "Failed to parse {side} price at index {index} for {instrument_name}: {level:?}"
143 );
144 None
145 })?;
146 let amount = level[1].as_f64().or_else(|| {
147 log::warn!(
148 "Failed to parse {side} amount at index {index} for {instrument_name}: {level:?}"
149 );
150 None
151 })?;
152 (price, amount)
153 } else {
154 log::warn!(
155 "Invalid {side} format at index {index} for {instrument_name}: expected 2-3 elements, was {}",
156 level.len()
157 );
158 return None;
159 };
160
161 if price_val <= 0.0 {
162 log::warn!(
163 "Invalid {side} price {price_val} at index {index} for {instrument_name}: {level:?}"
164 );
165 return None;
166 }
167
168 Some((price_val, amount_val))
169}
170
171fn parse_delta_level(
172 level: &[serde_json::Value],
173 index: usize,
174 side: &str,
175 instrument_name: &str,
176) -> Option<(BookAction, f64, f64)> {
177 if level.len() < 3 {
178 log::warn!(
179 "Invalid {side} delta format at index {index} for {instrument_name}: expected 3 elements, was {}",
180 level.len()
181 );
182 return None;
183 }
184
185 let action_str = level[0].as_str().or_else(|| {
186 log::warn!(
187 "Failed to parse {side} action at index {index} for {instrument_name}: {level:?}"
188 );
189 None
190 })?;
191
192 let deribit_action: DeribitBookAction = action_str.parse().ok().or_else(|| {
193 log::warn!(
194 "Unknown {side} action '{action_str}' at index {index} for {instrument_name}: {level:?}"
195 );
196 None
197 })?;
198
199 let price_val = level[1].as_f64().or_else(|| {
200 log::warn!(
201 "Failed to parse {side} price at index {index} for {instrument_name}: {level:?}"
202 );
203 None
204 })?;
205
206 let amount_val = level[2].as_f64().or_else(|| {
207 log::warn!(
208 "Failed to parse {side} amount at index {index} for {instrument_name}: {level:?}"
209 );
210 None
211 })?;
212
213 if price_val <= 0.0 {
214 log::warn!(
215 "Invalid {side} price {price_val} at index {index} for {instrument_name}: {level:?}"
216 );
217 return None;
218 }
219
220 Some((deribit_action.into(), price_val, amount_val))
221}
222
223pub fn parse_book_snapshot(
229 msg: &DeribitBookMsg,
230 instrument: &InstrumentAny,
231 ts_init: UnixNanos,
232) -> anyhow::Result<OrderBookDeltas> {
233 let instrument_id = instrument.id();
234 let price_precision = instrument.price_precision();
235 let size_precision = instrument.size_precision();
236 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
237
238 let mut deltas = Vec::new();
239
240 let has_levels = !msg.bids.is_empty() || !msg.asks.is_empty();
241
242 let clear_flags = if has_levels {
244 RecordFlag::F_SNAPSHOT as u8
245 } else {
246 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
247 };
248
249 deltas.push(OrderBookDelta::new(
250 instrument_id,
251 BookAction::Clear,
252 BookOrder::default(),
253 clear_flags,
254 msg.change_id,
255 ts_event,
256 ts_init,
257 ));
258
259 for (i, bid) in msg.bids.iter().enumerate() {
260 let Some((price_val, amount_val)) =
261 parse_snapshot_level(bid, i, "bid", msg.instrument_name.as_str())
262 else {
263 continue;
264 };
265
266 if amount_val > 0.0 {
267 let price = Price::new(price_val, price_precision);
268 let size = Quantity::new(amount_val, size_precision);
269
270 deltas.push(OrderBookDelta::new(
271 instrument_id,
272 BookAction::Add,
273 BookOrder::new(OrderSide::Buy, price, size, i as u64),
274 RecordFlag::F_SNAPSHOT as u8,
275 msg.change_id,
276 ts_event,
277 ts_init,
278 ));
279 }
280 }
281
282 let num_bids = msg.bids.len();
283 for (i, ask) in msg.asks.iter().enumerate() {
284 let Some((price_val, amount_val)) =
285 parse_snapshot_level(ask, i, "ask", msg.instrument_name.as_str())
286 else {
287 continue;
288 };
289
290 if amount_val > 0.0 {
291 let price = Price::new(price_val, price_precision);
292 let size = Quantity::new(amount_val, size_precision);
293
294 deltas.push(OrderBookDelta::new(
295 instrument_id,
296 BookAction::Add,
297 BookOrder::new(OrderSide::Sell, price, size, (num_bids + i) as u64),
298 RecordFlag::F_SNAPSHOT as u8,
299 msg.change_id,
300 ts_event,
301 ts_init,
302 ));
303 }
304 }
305
306 if let Some(last) = deltas.last_mut() {
307 *last = OrderBookDelta::new(
308 last.instrument_id,
309 last.action,
310 last.order,
311 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8,
312 last.sequence,
313 last.ts_event,
314 last.ts_init,
315 );
316 }
317
318 Ok(OrderBookDeltas::new(instrument_id, deltas))
319}
320
321pub fn parse_book_delta(
327 msg: &DeribitBookMsg,
328 instrument: &InstrumentAny,
329 ts_init: UnixNanos,
330) -> anyhow::Result<OrderBookDeltas> {
331 let instrument_id = instrument.id();
332 let price_precision = instrument.price_precision();
333 let size_precision = instrument.size_precision();
334 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
335
336 let mut deltas = Vec::new();
337
338 for (i, bid) in msg.bids.iter().enumerate() {
339 let Some((action, price_val, amount_val)) =
340 parse_delta_level(bid, i, "bid", msg.instrument_name.as_str())
341 else {
342 continue;
343 };
344
345 let price = Price::new(price_val, price_precision);
346 let size = Quantity::new(amount_val.abs(), size_precision);
347
348 deltas.push(OrderBookDelta::new(
349 instrument_id,
350 action,
351 BookOrder::new(OrderSide::Buy, price, size, i as u64),
352 0,
353 msg.change_id,
354 ts_event,
355 ts_init,
356 ));
357 }
358
359 let num_bids = msg.bids.len();
360 for (i, ask) in msg.asks.iter().enumerate() {
361 let Some((action, price_val, amount_val)) =
362 parse_delta_level(ask, i, "ask", msg.instrument_name.as_str())
363 else {
364 continue;
365 };
366
367 let price = Price::new(price_val, price_precision);
368 let size = Quantity::new(amount_val.abs(), size_precision);
369
370 deltas.push(OrderBookDelta::new(
371 instrument_id,
372 action,
373 BookOrder::new(OrderSide::Sell, price, size, (num_bids + i) as u64),
374 0,
375 msg.change_id,
376 ts_event,
377 ts_init,
378 ));
379 }
380
381 if let Some(last) = deltas.last_mut() {
383 *last = OrderBookDelta::new(
384 last.instrument_id,
385 last.action,
386 last.order,
387 RecordFlag::F_LAST as u8,
388 last.sequence,
389 last.ts_event,
390 last.ts_init,
391 );
392 }
393
394 Ok(OrderBookDeltas::new(instrument_id, deltas))
395}
396
397pub fn parse_book_msg(
403 msg: &DeribitBookMsg,
404 instrument: &InstrumentAny,
405 ts_init: UnixNanos,
406) -> anyhow::Result<OrderBookDeltas> {
407 match msg.msg_type {
408 DeribitBookMsgType::Snapshot => parse_book_snapshot(msg, instrument, ts_init),
409 DeribitBookMsgType::Change => parse_book_delta(msg, instrument, ts_init),
410 }
411}
412
413pub fn parse_ticker_to_quote(
419 msg: &DeribitTickerMsg,
420 instrument: &InstrumentAny,
421 ts_init: UnixNanos,
422) -> anyhow::Result<QuoteTick> {
423 let instrument_id = instrument.id();
424 let price_precision = instrument.price_precision();
425 let size_precision = instrument.size_precision();
426
427 let bid_price_val = msg
428 .best_bid_price
429 .context("Missing best_bid_price in ticker")?;
430 let ask_price_val = msg
431 .best_ask_price
432 .context("Missing best_ask_price in ticker")?;
433
434 let bid_price = Price::from_decimal_dp(bid_price_val, price_precision)?;
435 let ask_price = Price::from_decimal_dp(ask_price_val, price_precision)?;
436 let bid_size =
437 Quantity::from_decimal_dp(msg.best_bid_amount.unwrap_or_default(), size_precision)?;
438 let ask_size =
439 Quantity::from_decimal_dp(msg.best_ask_amount.unwrap_or_default(), size_precision)?;
440 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
441
442 QuoteTick::new_checked(
443 instrument_id,
444 bid_price,
445 ask_price,
446 bid_size,
447 ask_size,
448 ts_event,
449 ts_init,
450 )
451}
452
453pub fn parse_quote_msg(
459 msg: &DeribitQuoteMsg,
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 = Price::from_decimal_dp(msg.best_bid_price, price_precision)?;
468 let ask_price = Price::from_decimal_dp(msg.best_ask_price, price_precision)?;
469 let bid_size = Quantity::from_decimal_dp(msg.best_bid_amount, size_precision)?;
470 let ask_size = Quantity::from_decimal_dp(msg.best_ask_amount, size_precision)?;
471 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
472
473 QuoteTick::new_checked(
474 instrument_id,
475 bid_price,
476 ask_price,
477 bid_size,
478 ask_size,
479 ts_event,
480 ts_init,
481 )
482}
483
484pub fn parse_ticker_to_mark_price(
490 msg: &DeribitTickerMsg,
491 instrument: &InstrumentAny,
492 ts_init: UnixNanos,
493) -> anyhow::Result<MarkPriceUpdate> {
494 let instrument_id = instrument.id();
495 let price_precision = instrument.price_precision();
496 let value = Price::from_decimal_dp(msg.mark_price, price_precision)?;
497 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
498
499 Ok(MarkPriceUpdate::new(
500 instrument_id,
501 value,
502 ts_event,
503 ts_init,
504 ))
505}
506
507pub fn parse_ticker_to_index_price(
513 msg: &DeribitTickerMsg,
514 instrument: &InstrumentAny,
515 ts_init: UnixNanos,
516) -> anyhow::Result<IndexPriceUpdate> {
517 let instrument_id = instrument.id();
518 let price_precision = instrument.price_precision();
519 let value = Price::from_decimal_dp(msg.index_price, price_precision)?;
520 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
521
522 Ok(IndexPriceUpdate::new(
523 instrument_id,
524 value,
525 ts_event,
526 ts_init,
527 ))
528}
529
530#[must_use]
534pub fn parse_ticker_to_funding_rate(
535 msg: &DeribitTickerMsg,
536 instrument: &InstrumentAny,
537 ts_init: UnixNanos,
538) -> Option<FundingRateUpdate> {
539 let rate = msg.current_funding?;
541 let instrument_id = instrument.id();
542 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
543
544 Some(FundingRateUpdate::new(
546 instrument_id,
547 rate,
548 None, ts_event,
550 ts_init,
551 ))
552}
553
554#[must_use]
559pub fn parse_perpetual_to_funding_rate(
560 msg: &DeribitPerpetualMsg,
561 instrument: &InstrumentAny,
562 ts_init: UnixNanos,
563) -> FundingRateUpdate {
564 let instrument_id = instrument.id();
565 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
566
567 FundingRateUpdate::new(
568 instrument_id,
569 msg.interest,
570 None, ts_event,
572 ts_init,
573 )
574}
575
576pub fn resolution_to_bar_type(
584 instrument_id: InstrumentId,
585 resolution: &str,
586) -> anyhow::Result<BarType> {
587 let (step, aggregation) = match resolution {
588 "1" => (1, BarAggregation::Minute),
589 "3" => (3, BarAggregation::Minute),
590 "5" => (5, BarAggregation::Minute),
591 "10" => (10, BarAggregation::Minute),
592 "15" => (15, BarAggregation::Minute),
593 "30" => (30, BarAggregation::Minute),
594 "60" => (60, BarAggregation::Minute),
595 "120" => (120, BarAggregation::Minute),
596 "180" => (180, BarAggregation::Minute),
597 "360" => (360, BarAggregation::Minute),
598 "720" => (720, BarAggregation::Minute),
599 "1D" => (1, BarAggregation::Day),
600 _ => anyhow::bail!("Unsupported Deribit resolution: {resolution}"),
601 };
602
603 let spec = BarSpecification::new(step, aggregation, PriceType::Last);
604 Ok(BarType::new(
605 instrument_id,
606 spec,
607 AggregationSource::External,
608 ))
609}
610
611pub fn parse_chart_msg(
622 chart_msg: &DeribitChartMsg,
623 bar_type: BarType,
624 price_precision: u8,
625 size_precision: u8,
626 timestamp_on_close: bool,
627 ts_init: UnixNanos,
628) -> anyhow::Result<Bar> {
629 let open = Price::new_checked(chart_msg.open, price_precision).context("Invalid open price")?;
630 let high = Price::new_checked(chart_msg.high, price_precision).context("Invalid high price")?;
631 let low = Price::new_checked(chart_msg.low, price_precision).context("Invalid low price")?;
632 let close =
633 Price::new_checked(chart_msg.close, price_precision).context("Invalid close price")?;
634 let volume =
635 Quantity::new_checked(chart_msg.volume, size_precision).context("Invalid volume")?;
636
637 let mut ts_event = UnixNanos::from(chart_msg.tick * NANOSECONDS_IN_MILLISECOND);
639
640 if timestamp_on_close {
642 let interval_ns = bar_type
643 .spec()
644 .timedelta()
645 .num_nanoseconds()
646 .context("bar specification produced non-integer interval")?;
647 let interval_ns = u64::try_from(interval_ns)
648 .context("bar interval overflowed the u64 range for nanoseconds")?;
649 let updated = ts_event
650 .as_u64()
651 .checked_add(interval_ns)
652 .context("bar timestamp overflowed when adjusting to close time")?;
653 ts_event = UnixNanos::from(updated);
654 }
655
656 Bar::new_checked(bar_type, open, high, low, close, volume, ts_event, ts_init)
657 .context("Invalid OHLC bar")
658}
659
660pub fn parse_user_order_msg(
666 msg: &DeribitOrderMsg,
667 instrument: &InstrumentAny,
668 account_id: AccountId,
669 ts_init: UnixNanos,
670) -> anyhow::Result<OrderStatusReport> {
671 let instrument_id = instrument.id();
672 let venue_order_id = VenueOrderId::new(&msg.order_id);
673
674 let order_side = match msg.direction.as_str() {
675 "buy" => OrderSide::Buy,
676 "sell" => OrderSide::Sell,
677 _ => anyhow::bail!("Unknown order direction: {}", msg.direction),
678 };
679
680 let order_type = match msg.order_type.as_str() {
682 "limit" => OrderType::Limit,
683 "market" => OrderType::Market,
684 "stop_limit" => OrderType::StopLimit,
685 "stop_market" => OrderType::StopMarket,
686 "take_limit" => OrderType::LimitIfTouched,
687 "take_market" => OrderType::MarketIfTouched,
688 _ => OrderType::Limit, };
690
691 let time_in_force = match msg.time_in_force.as_str() {
693 "good_til_cancelled" => TimeInForce::Gtc,
694 "good_til_day" => TimeInForce::Gtd,
695 "fill_or_kill" => TimeInForce::Fok,
696 "immediate_or_cancel" => TimeInForce::Ioc,
697 other => {
698 log::warn!("Unknown time_in_force '{other}', defaulting to GTC");
699 TimeInForce::Gtc
700 }
701 };
702
703 let order_status = match msg.order_state.as_str() {
705 "open" => {
706 if msg.filled_amount.is_zero() {
707 OrderStatus::Accepted
708 } else {
709 OrderStatus::PartiallyFilled
710 }
711 }
712 "filled" => OrderStatus::Filled,
713 "rejected" => OrderStatus::Rejected,
714 "cancelled" => OrderStatus::Canceled,
715 "untriggered" => OrderStatus::Accepted, _ => OrderStatus::Accepted,
717 };
718
719 let price_precision = instrument.price_precision();
720 let size_precision = instrument.size_precision();
721
722 let quantity = Quantity::from_decimal_dp(msg.amount, size_precision)?;
723 let filled_qty = Quantity::from_decimal_dp(msg.filled_amount, size_precision)?;
724
725 let ts_accepted = UnixNanos::new(msg.creation_timestamp * NANOSECONDS_IN_MILLISECOND);
726 let ts_last = UnixNanos::new(msg.last_update_timestamp * NANOSECONDS_IN_MILLISECOND);
727
728 let mut report = OrderStatusReport::new(
729 account_id,
730 instrument_id,
731 None, venue_order_id,
733 order_side,
734 order_type,
735 time_in_force,
736 order_status,
737 quantity,
738 filled_qty,
739 ts_accepted,
740 ts_last,
741 ts_init,
742 Some(UUID4::new()),
743 );
744
745 if let Some(ref label) = msg.label
747 && !label.is_empty()
748 {
749 report = report.with_client_order_id(ClientOrderId::new(label));
750 }
751
752 if let Some(price_val) = msg.price
754 && !price_val.is_zero()
755 {
756 let price = Price::from_decimal_dp(price_val, price_precision)?;
757 report = report.with_price(price);
758 }
759
760 if time_in_force == TimeInForce::Gtd {
761 let expire_time = next_8_utc(ts_accepted);
762 report = report.with_expire_time(expire_time);
763 }
764
765 if let Some(avg_price) = msg.average_price
767 && !avg_price.is_zero()
768 {
769 report = report.with_avg_px(avg_price.to_f64().unwrap_or_default())?;
770 }
771
772 if let Some(trigger_price) = msg.trigger_price
774 && !trigger_price.is_zero()
775 {
776 let trigger = Price::from_decimal_dp(trigger_price, price_precision)?;
777 report = report.with_trigger_price(trigger);
778 }
779
780 if msg.post_only {
781 report = report.with_post_only(true);
782 }
783
784 if msg.reduce_only {
785 report = report.with_reduce_only(true);
786 }
787
788 if let Some(ref reason) = msg.reject_reason {
790 report = report.with_cancel_reason(reason.clone());
791 } else if let Some(ref reason) = msg.cancel_reason {
792 report = report.with_cancel_reason(reason.clone());
793 }
794
795 Ok(report)
796}
797
798pub fn parse_user_trade_msg(
804 msg: &DeribitUserTradeMsg,
805 instrument: &InstrumentAny,
806 account_id: AccountId,
807 ts_init: UnixNanos,
808) -> anyhow::Result<FillReport> {
809 let instrument_id = instrument.id();
810 let venue_order_id = VenueOrderId::new(&msg.order_id);
811 let trade_id = TradeId::new(&msg.trade_id);
812
813 let order_side = match msg.direction.as_str() {
814 "buy" => OrderSide::Buy,
815 "sell" => OrderSide::Sell,
816 _ => anyhow::bail!("Unknown trade direction: {}", msg.direction),
817 };
818
819 let price_precision = instrument.price_precision();
820 let size_precision = instrument.size_precision();
821
822 let last_qty = Quantity::from_decimal_dp(msg.amount, size_precision)?;
823 let last_px = Price::from_decimal_dp(msg.price, price_precision)?;
824
825 let liquidity_side = match msg.liquidity.as_str() {
826 "M" => LiquiditySide::Maker,
827 "T" => LiquiditySide::Taker,
828 _ => LiquiditySide::NoLiquiditySide,
829 };
830
831 let fee_currency = Currency::from(&msg.fee_currency);
833 let commission = Money::from_decimal(msg.fee, fee_currency)?;
834
835 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
836
837 let client_order_id = msg
838 .label
839 .as_ref()
840 .filter(|l| !l.is_empty())
841 .map(ClientOrderId::new);
842
843 Ok(FillReport::new(
844 account_id,
845 instrument_id,
846 venue_order_id,
847 trade_id,
848 order_side,
849 last_qty,
850 last_px,
851 commission,
852 liquidity_side,
853 client_order_id,
854 None, ts_event,
856 ts_init,
857 None, ))
859}
860
861#[must_use]
872pub fn parse_position_status_report(
873 position: &DeribitPosition,
874 instrument: &InstrumentAny,
875 account_id: AccountId,
876 ts_init: UnixNanos,
877) -> PositionStatusReport {
878 let instrument_id = instrument.id();
879 let size_precision = instrument.size_precision();
880
881 let signed_qty = Quantity::from_decimal_dp(position.size.abs(), size_precision)
882 .unwrap_or_else(|_| Quantity::new(0.0, size_precision));
883
884 let position_side = match position.direction.as_str() {
885 "buy" => PositionSideSpecified::Long,
886 "sell" => PositionSideSpecified::Short,
887 _ => PositionSideSpecified::Flat,
888 };
889
890 let avg_px_open = Some(position.average_price);
892
893 PositionStatusReport::new(
894 account_id,
895 instrument_id,
896 position_side,
897 signed_qty,
898 ts_init,
899 ts_init,
900 Some(UUID4::new()),
901 None, avg_px_open,
903 )
904}
905
906#[derive(Debug, Clone)]
911pub enum ParsedOrderEvent {
912 Accepted(OrderAccepted),
914 Canceled(OrderCanceled),
916 Expired(OrderExpired),
918 Updated(OrderUpdated),
920 None,
922}
923
924fn extract_client_order_id(msg: &DeribitOrderMsg) -> Option<ClientOrderId> {
926 msg.label
927 .as_ref()
928 .filter(|l| !l.is_empty())
929 .map(ClientOrderId::new)
930}
931
932#[must_use]
937pub fn parse_order_accepted(
938 msg: &DeribitOrderMsg,
939 instrument: &InstrumentAny,
940 account_id: AccountId,
941 trader_id: TraderId,
942 strategy_id: StrategyId,
943 ts_init: UnixNanos,
944) -> OrderAccepted {
945 let instrument_id = instrument.id();
946 let venue_order_id = VenueOrderId::new(&msg.order_id);
947 let client_order_id = extract_client_order_id(msg).unwrap_or_else(|| {
948 ClientOrderId::new(&msg.order_id)
950 });
951 let ts_event = UnixNanos::new(msg.last_update_timestamp * NANOSECONDS_IN_MILLISECOND);
952
953 OrderAccepted::new(
954 trader_id,
955 strategy_id,
956 instrument_id,
957 client_order_id,
958 venue_order_id,
959 account_id,
960 nautilus_core::UUID4::new(),
961 ts_event,
962 ts_init,
963 false, )
965}
966
967#[must_use]
971pub fn parse_order_canceled(
972 msg: &DeribitOrderMsg,
973 instrument: &InstrumentAny,
974 account_id: AccountId,
975 trader_id: TraderId,
976 strategy_id: StrategyId,
977 ts_init: UnixNanos,
978) -> OrderCanceled {
979 let instrument_id = instrument.id();
980 let venue_order_id = VenueOrderId::new(&msg.order_id);
981 let client_order_id =
982 extract_client_order_id(msg).unwrap_or_else(|| ClientOrderId::new(&msg.order_id));
983 let ts_event = UnixNanos::new(msg.last_update_timestamp * NANOSECONDS_IN_MILLISECOND);
984
985 OrderCanceled::new(
986 trader_id,
987 strategy_id,
988 instrument_id,
989 client_order_id,
990 nautilus_core::UUID4::new(),
991 ts_event,
992 ts_init,
993 false, Some(venue_order_id),
995 Some(account_id),
996 )
997}
998
999#[must_use]
1004pub fn parse_order_expired(
1005 msg: &DeribitOrderMsg,
1006 instrument: &InstrumentAny,
1007 account_id: AccountId,
1008 trader_id: TraderId,
1009 strategy_id: StrategyId,
1010 ts_init: UnixNanos,
1011) -> OrderExpired {
1012 let instrument_id = instrument.id();
1013 let venue_order_id = VenueOrderId::new(&msg.order_id);
1014 let client_order_id =
1015 extract_client_order_id(msg).unwrap_or_else(|| ClientOrderId::new(&msg.order_id));
1016 let ts_event = UnixNanos::new(msg.last_update_timestamp * NANOSECONDS_IN_MILLISECOND);
1017
1018 OrderExpired::new(
1019 trader_id,
1020 strategy_id,
1021 instrument_id,
1022 client_order_id,
1023 nautilus_core::UUID4::new(),
1024 ts_event,
1025 ts_init,
1026 false, Some(venue_order_id),
1028 Some(account_id),
1029 )
1030}
1031
1032#[must_use]
1036pub fn parse_order_updated(
1037 msg: &DeribitOrderMsg,
1038 instrument: &InstrumentAny,
1039 account_id: AccountId,
1040 trader_id: TraderId,
1041 strategy_id: StrategyId,
1042 ts_init: UnixNanos,
1043) -> OrderUpdated {
1044 let instrument_id = instrument.id();
1045 let price_precision = instrument.price_precision();
1046 let size_precision = instrument.size_precision();
1047
1048 let venue_order_id = VenueOrderId::new(&msg.order_id);
1049 let client_order_id =
1050 extract_client_order_id(msg).unwrap_or_else(|| ClientOrderId::new(&msg.order_id));
1051 let quantity = Quantity::from_decimal_dp(msg.amount, size_precision)
1052 .unwrap_or_else(|_| Quantity::new(0.0, size_precision));
1053 let price = msg
1054 .price
1055 .and_then(|p| Price::from_decimal_dp(p, price_precision).ok());
1056 let trigger_price = msg
1057 .trigger_price
1058 .and_then(|p| Price::from_decimal_dp(p, price_precision).ok());
1059 let ts_event = UnixNanos::new(msg.last_update_timestamp * NANOSECONDS_IN_MILLISECOND);
1060
1061 OrderUpdated::new(
1062 trader_id,
1063 strategy_id,
1064 instrument_id,
1065 client_order_id,
1066 quantity,
1067 nautilus_core::UUID4::new(),
1068 ts_event,
1069 ts_init,
1070 false, Some(venue_order_id),
1072 Some(account_id),
1073 price,
1074 trigger_price,
1075 None, )
1077}
1078
1079#[must_use]
1092pub fn determine_order_event_type(
1093 order_state: &str,
1094 is_new_order: bool,
1095 was_amended: bool,
1096) -> OrderEventType {
1097 match order_state {
1098 "open" | "untriggered" => {
1099 if was_amended {
1100 OrderEventType::Updated
1101 } else if is_new_order {
1102 OrderEventType::Accepted
1103 } else {
1104 OrderEventType::None
1106 }
1107 }
1108 "cancelled" => OrderEventType::Canceled,
1109 "expired" => OrderEventType::Expired,
1110 "filled" => {
1111 OrderEventType::None
1113 }
1114 "rejected" => {
1115 OrderEventType::None
1117 }
1118 _ => OrderEventType::None,
1119 }
1120}
1121
1122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1124pub enum OrderEventType {
1125 Accepted,
1127 Canceled,
1129 Expired,
1131 Updated,
1133 None,
1135}
1136
1137#[cfg(test)]
1138mod tests {
1139 use rstest::rstest;
1140 use rust_decimal_macros::dec;
1141
1142 use super::*;
1143 use crate::{
1144 common::{parse::parse_deribit_instrument_any, testing::load_test_json},
1145 http::models::{DeribitInstrument, DeribitJsonRpcResponse},
1146 };
1147
1148 fn test_perpetual_instrument() -> InstrumentAny {
1150 let json = load_test_json("http_get_instruments.json");
1151 let response: DeribitJsonRpcResponse<Vec<DeribitInstrument>> =
1152 serde_json::from_str(&json).unwrap();
1153 let instrument = &response.result.unwrap()[0];
1154 parse_deribit_instrument_any(instrument, UnixNanos::default(), UnixNanos::default())
1155 .unwrap()
1156 .unwrap()
1157 }
1158
1159 #[rstest]
1160 fn test_parse_trade_msg_sell() {
1161 let instrument = test_perpetual_instrument();
1162 let json = load_test_json("ws_trades.json");
1163 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1164 let trades: Vec<DeribitTradeMsg> =
1165 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1166 let msg = &trades[0];
1167
1168 let tick = parse_trade_msg(msg, &instrument, UnixNanos::default()).unwrap();
1169
1170 assert_eq!(tick.instrument_id, instrument.id());
1171 assert_eq!(tick.price, instrument.make_price(92294.5));
1172 assert_eq!(tick.size, instrument.make_qty(10.0, None));
1173 assert_eq!(tick.aggressor_side, AggressorSide::Seller);
1174 assert_eq!(tick.trade_id.to_string(), "403691824");
1175 assert_eq!(tick.ts_event, UnixNanos::new(1_765_531_356_452_000_000));
1176 }
1177
1178 #[rstest]
1179 fn test_parse_trade_msg_buy() {
1180 let instrument = test_perpetual_instrument();
1181 let json = load_test_json("ws_trades.json");
1182 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1183 let trades: Vec<DeribitTradeMsg> =
1184 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1185 let msg = &trades[1];
1186
1187 let tick = parse_trade_msg(msg, &instrument, UnixNanos::default()).unwrap();
1188
1189 assert_eq!(tick.instrument_id, instrument.id());
1190 assert_eq!(tick.price, instrument.make_price(92288.5));
1191 assert_eq!(tick.size, instrument.make_qty(750.0, None));
1192 assert_eq!(tick.aggressor_side, AggressorSide::Seller);
1193 assert_eq!(tick.trade_id.to_string(), "403691825");
1194 }
1195
1196 #[rstest]
1197 fn test_parse_book_snapshot() {
1198 let instrument = test_perpetual_instrument();
1199 let json = load_test_json("ws_book_snapshot.json");
1200 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1201 let msg: DeribitBookMsg =
1202 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1203
1204 let deltas = parse_book_snapshot(&msg, &instrument, UnixNanos::default()).unwrap();
1205
1206 assert_eq!(deltas.instrument_id, instrument.id());
1207 assert_eq!(deltas.deltas.len(), 11);
1209
1210 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
1212
1213 let first_bid = &deltas.deltas[1];
1215 assert_eq!(first_bid.action, BookAction::Add);
1216 assert_eq!(first_bid.order.side, OrderSide::Buy);
1217 assert_eq!(first_bid.order.price, instrument.make_price(42500.0));
1218 assert_eq!(first_bid.order.size, instrument.make_qty(1000.0, None));
1219
1220 let first_ask = &deltas.deltas[6];
1222 assert_eq!(first_ask.action, BookAction::Add);
1223 assert_eq!(first_ask.order.side, OrderSide::Sell);
1224 assert_eq!(first_ask.order.price, instrument.make_price(42501.0));
1225 assert_eq!(first_ask.order.size, instrument.make_qty(800.0, None));
1226
1227 let last = deltas.deltas.last().unwrap();
1229 assert_eq!(
1230 last.flags & RecordFlag::F_LAST as u8,
1231 RecordFlag::F_LAST as u8
1232 );
1233 }
1234
1235 #[rstest]
1236 fn test_parse_book_delta() {
1237 let instrument = test_perpetual_instrument();
1238 let json = load_test_json("ws_book_delta.json");
1239 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1240 let msg: DeribitBookMsg =
1241 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1242
1243 let deltas = parse_book_delta(&msg, &instrument, UnixNanos::default()).unwrap();
1244
1245 assert_eq!(deltas.instrument_id, instrument.id());
1246 assert_eq!(deltas.deltas.len(), 4);
1248
1249 let bid_change = &deltas.deltas[0];
1251 assert_eq!(bid_change.action, BookAction::Update);
1252 assert_eq!(bid_change.order.side, OrderSide::Buy);
1253 assert_eq!(bid_change.order.price, instrument.make_price(42500.0));
1254 assert_eq!(bid_change.order.size, instrument.make_qty(950.0, None));
1255
1256 let bid_new = &deltas.deltas[1];
1258 assert_eq!(bid_new.action, BookAction::Add);
1259 assert_eq!(bid_new.order.side, OrderSide::Buy);
1260 assert_eq!(bid_new.order.price, instrument.make_price(42498.5));
1261 assert_eq!(bid_new.order.size, instrument.make_qty(300.0, None));
1262
1263 let ask_delete = &deltas.deltas[2];
1265 assert_eq!(ask_delete.action, BookAction::Delete);
1266 assert_eq!(ask_delete.order.side, OrderSide::Sell);
1267 assert_eq!(ask_delete.order.price, instrument.make_price(42501.0));
1268 assert_eq!(ask_delete.order.size, instrument.make_qty(0.0, None));
1269
1270 let ask_change = &deltas.deltas[3];
1272 assert_eq!(ask_change.action, BookAction::Update);
1273 assert_eq!(ask_change.order.side, OrderSide::Sell);
1274 assert_eq!(ask_change.order.price, instrument.make_price(42501.5));
1275 assert_eq!(ask_change.order.size, instrument.make_qty(700.0, None));
1276
1277 let last = deltas.deltas.last().unwrap();
1279 assert_eq!(
1280 last.flags & RecordFlag::F_LAST as u8,
1281 RecordFlag::F_LAST as u8
1282 );
1283 }
1284
1285 #[rstest]
1286 fn test_parse_ticker_to_quote() {
1287 let instrument = test_perpetual_instrument();
1288 let json = load_test_json("ws_ticker.json");
1289 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1290 let msg: DeribitTickerMsg =
1291 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1292
1293 assert_eq!(msg.instrument_name.as_str(), "BTC-PERPETUAL");
1295 assert_eq!(msg.timestamp, 1_765_541_474_086);
1296 assert_eq!(msg.best_bid_price, Some(dec!(92283.5)));
1297 assert_eq!(msg.best_ask_price, Some(dec!(92284.0)));
1298 assert_eq!(msg.best_bid_amount, Some(dec!(117660.0)));
1299 assert_eq!(msg.best_ask_amount, Some(dec!(186520.0)));
1300 assert_eq!(msg.mark_price, dec!(92281.78));
1301 assert_eq!(msg.index_price, dec!(92263.55));
1302 assert_eq!(msg.open_interest, dec!(1132329370.0));
1303
1304 let quote = parse_ticker_to_quote(&msg, &instrument, UnixNanos::default()).unwrap();
1305
1306 assert_eq!(quote.instrument_id, instrument.id());
1307 assert_eq!(quote.bid_price, instrument.make_price(92283.5));
1308 assert_eq!(quote.ask_price, instrument.make_price(92284.0));
1309 assert_eq!(quote.bid_size, instrument.make_qty(117660.0, None));
1310 assert_eq!(quote.ask_size, instrument.make_qty(186520.0, None));
1311 assert_eq!(quote.ts_event, UnixNanos::new(1_765_541_474_086_000_000));
1312 }
1313
1314 #[rstest]
1315 fn test_parse_quote_msg() {
1316 let instrument = test_perpetual_instrument();
1317 let json = load_test_json("ws_quote.json");
1318 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1319 let msg: DeribitQuoteMsg =
1320 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1321
1322 assert_eq!(msg.instrument_name.as_str(), "BTC-PERPETUAL");
1324 assert_eq!(msg.timestamp, 1_765_541_767_174);
1325 assert_eq!(msg.best_bid_price, dec!(92288.0));
1326 assert_eq!(msg.best_ask_price, dec!(92288.5));
1327 assert_eq!(msg.best_bid_amount, dec!(133440.0));
1328 assert_eq!(msg.best_ask_amount, dec!(99470.0));
1329
1330 let quote = parse_quote_msg(&msg, &instrument, UnixNanos::default()).unwrap();
1331
1332 assert_eq!(quote.instrument_id, instrument.id());
1333 assert_eq!(quote.bid_price, instrument.make_price(92288.0));
1334 assert_eq!(quote.ask_price, instrument.make_price(92288.5));
1335 assert_eq!(quote.bid_size, instrument.make_qty(133440.0, None));
1336 assert_eq!(quote.ask_size, instrument.make_qty(99470.0, None));
1337 assert_eq!(quote.ts_event, UnixNanos::new(1_765_541_767_174_000_000));
1338 }
1339
1340 #[rstest]
1341 fn test_parse_book_msg_snapshot() {
1342 let instrument = test_perpetual_instrument();
1343 let json = load_test_json("ws_book_snapshot.json");
1344 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1345 let msg: DeribitBookMsg =
1346 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1347
1348 assert_eq!(
1350 msg.bids[0].len(),
1351 3,
1352 "Snapshot bids should have 3 elements: [action, price, amount]"
1353 );
1354 assert_eq!(
1355 msg.bids[0][0].as_str(),
1356 Some("new"),
1357 "First element should be 'new' action for snapshot"
1358 );
1359 assert_eq!(
1360 msg.asks[0].len(),
1361 3,
1362 "Snapshot asks should have 3 elements: [action, price, amount]"
1363 );
1364 assert_eq!(
1365 msg.asks[0][0].as_str(),
1366 Some("new"),
1367 "First element should be 'new' action for snapshot"
1368 );
1369
1370 let deltas = parse_book_msg(&msg, &instrument, UnixNanos::default()).unwrap();
1371
1372 assert_eq!(deltas.instrument_id, instrument.id());
1373 assert_eq!(deltas.deltas.len(), 11);
1375
1376 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
1378
1379 let first_bid = &deltas.deltas[1];
1381 assert_eq!(first_bid.action, BookAction::Add);
1382 assert_eq!(first_bid.order.side, OrderSide::Buy);
1383 assert_eq!(first_bid.order.price, instrument.make_price(42500.0));
1384 assert_eq!(first_bid.order.size, instrument.make_qty(1000.0, None));
1385
1386 let first_ask = &deltas.deltas[6];
1388 assert_eq!(first_ask.action, BookAction::Add);
1389 assert_eq!(first_ask.order.side, OrderSide::Sell);
1390 assert_eq!(first_ask.order.price, instrument.make_price(42501.0));
1391 assert_eq!(first_ask.order.size, instrument.make_qty(800.0, None));
1392 }
1393
1394 #[rstest]
1395 fn test_parse_book_msg_delta() {
1396 let instrument = test_perpetual_instrument();
1397 let json = load_test_json("ws_book_delta.json");
1398 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1399 let msg: DeribitBookMsg =
1400 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1401
1402 assert_eq!(
1404 msg.bids[0].len(),
1405 3,
1406 "Delta bids should have 3 elements: [action, price, amount]"
1407 );
1408 assert_eq!(
1409 msg.bids[0][0].as_str(),
1410 Some("change"),
1411 "First bid should be 'change' action"
1412 );
1413 assert_eq!(
1414 msg.bids[1][0].as_str(),
1415 Some("new"),
1416 "Second bid should be 'new' action"
1417 );
1418 assert_eq!(
1419 msg.asks[0].len(),
1420 3,
1421 "Delta asks should have 3 elements: [action, price, amount]"
1422 );
1423 assert_eq!(
1424 msg.asks[0][0].as_str(),
1425 Some("delete"),
1426 "First ask should be 'delete' action"
1427 );
1428
1429 let deltas = parse_book_msg(&msg, &instrument, UnixNanos::default()).unwrap();
1430
1431 assert_eq!(deltas.instrument_id, instrument.id());
1432 assert_eq!(deltas.deltas.len(), 4);
1434
1435 assert_ne!(deltas.deltas[0].action, BookAction::Clear);
1437
1438 let bid_change = &deltas.deltas[0];
1440 assert_eq!(bid_change.action, BookAction::Update);
1441 assert_eq!(bid_change.order.side, OrderSide::Buy);
1442 assert_eq!(bid_change.order.price, instrument.make_price(42500.0));
1443 assert_eq!(bid_change.order.size, instrument.make_qty(950.0, None));
1444
1445 let bid_new = &deltas.deltas[1];
1447 assert_eq!(bid_new.action, BookAction::Add);
1448 assert_eq!(bid_new.order.side, OrderSide::Buy);
1449 assert_eq!(bid_new.order.price, instrument.make_price(42498.5));
1450 assert_eq!(bid_new.order.size, instrument.make_qty(300.0, None));
1451
1452 let ask_delete = &deltas.deltas[2];
1454 assert_eq!(ask_delete.action, BookAction::Delete);
1455 assert_eq!(ask_delete.order.side, OrderSide::Sell);
1456 assert_eq!(ask_delete.order.price, instrument.make_price(42501.0));
1457
1458 let ask_change = &deltas.deltas[3];
1460 assert_eq!(ask_change.action, BookAction::Update);
1461 assert_eq!(ask_change.order.side, OrderSide::Sell);
1462 assert_eq!(ask_change.order.price, instrument.make_price(42501.5));
1463 assert_eq!(ask_change.order.size, instrument.make_qty(700.0, None));
1464 }
1465
1466 #[rstest]
1467 fn test_parse_book_grouped_snapshot() {
1468 let instrument = test_perpetual_instrument();
1471 let json = load_test_json("ws_book_grouped_snapshot.json");
1472 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1473 let msg: DeribitBookMsg =
1474 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1475
1476 assert_eq!(
1478 msg.bids[0].len(),
1479 2,
1480 "Grouped bids should have 2 elements: [price, amount]"
1481 );
1482 assert_eq!(
1483 msg.asks[0].len(),
1484 2,
1485 "Grouped asks should have 2 elements: [price, amount]"
1486 );
1487
1488 assert_eq!(
1490 msg.msg_type,
1491 DeribitBookMsgType::Snapshot,
1492 "Grouped channel should default to Snapshot type"
1493 );
1494
1495 let deltas = parse_book_snapshot(&msg, &instrument, UnixNanos::default()).unwrap();
1496
1497 assert_eq!(deltas.instrument_id, instrument.id());
1498 assert_eq!(deltas.deltas.len(), 21);
1500
1501 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
1503
1504 let first_bid = &deltas.deltas[1];
1506 assert_eq!(first_bid.action, BookAction::Add);
1507 assert_eq!(first_bid.order.side, OrderSide::Buy);
1508 assert_eq!(first_bid.order.price, instrument.make_price(89532.5));
1509 assert_eq!(first_bid.order.size, instrument.make_qty(254900.0, None));
1510
1511 let first_ask = &deltas.deltas[11];
1513 assert_eq!(first_ask.action, BookAction::Add);
1514 assert_eq!(first_ask.order.side, OrderSide::Sell);
1515 assert_eq!(first_ask.order.price, instrument.make_price(89533.0));
1516 assert_eq!(first_ask.order.size, instrument.make_qty(91570.0, None));
1517
1518 let last = deltas.deltas.last().unwrap();
1520 assert_eq!(
1521 last.flags & RecordFlag::F_LAST as u8,
1522 RecordFlag::F_LAST as u8
1523 );
1524 }
1525
1526 #[rstest]
1527 fn test_parse_ticker_to_mark_price() {
1528 let instrument = test_perpetual_instrument();
1529 let json = load_test_json("ws_ticker.json");
1530 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1531 let msg: DeribitTickerMsg =
1532 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1533
1534 let mark_price =
1535 parse_ticker_to_mark_price(&msg, &instrument, UnixNanos::default()).unwrap();
1536
1537 assert_eq!(mark_price.instrument_id, instrument.id());
1538 assert_eq!(mark_price.value, instrument.make_price(92281.78));
1539 assert_eq!(
1540 mark_price.ts_event,
1541 UnixNanos::new(1_765_541_474_086_000_000)
1542 );
1543 }
1544
1545 #[rstest]
1546 fn test_parse_ticker_to_index_price() {
1547 let instrument = test_perpetual_instrument();
1548 let json = load_test_json("ws_ticker.json");
1549 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1550 let msg: DeribitTickerMsg =
1551 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1552
1553 let index_price =
1554 parse_ticker_to_index_price(&msg, &instrument, UnixNanos::default()).unwrap();
1555
1556 assert_eq!(index_price.instrument_id, instrument.id());
1557 assert_eq!(index_price.value, instrument.make_price(92263.55));
1558 assert_eq!(
1559 index_price.ts_event,
1560 UnixNanos::new(1_765_541_474_086_000_000)
1561 );
1562 }
1563
1564 #[rstest]
1565 fn test_parse_ticker_to_funding_rate() {
1566 let instrument = test_perpetual_instrument();
1567 let json = load_test_json("ws_ticker.json");
1568 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1569 let msg: DeribitTickerMsg =
1570 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1571
1572 assert!(msg.current_funding.is_some());
1574
1575 let funding_rate =
1576 parse_ticker_to_funding_rate(&msg, &instrument, UnixNanos::default()).unwrap();
1577
1578 assert_eq!(funding_rate.instrument_id, instrument.id());
1579 assert_eq!(
1581 funding_rate.ts_event,
1582 UnixNanos::new(1_765_541_474_086_000_000)
1583 );
1584 assert!(funding_rate.next_funding_ns.is_none()); }
1586
1587 #[rstest]
1588 fn test_resolution_to_bar_type_1_minute() {
1589 let instrument_id = InstrumentId::from("BTC-PERPETUAL.DERIBIT");
1590 let bar_type = resolution_to_bar_type(instrument_id, "1").unwrap();
1591
1592 assert_eq!(bar_type.instrument_id(), instrument_id);
1593 assert_eq!(bar_type.spec().step.get(), 1);
1594 assert_eq!(bar_type.spec().aggregation, BarAggregation::Minute);
1595 assert_eq!(bar_type.spec().price_type, PriceType::Last);
1596 assert_eq!(bar_type.aggregation_source(), AggregationSource::External);
1597 }
1598
1599 #[rstest]
1600 fn test_resolution_to_bar_type_60_minute() {
1601 let instrument_id = InstrumentId::from("ETH-PERPETUAL.DERIBIT");
1602 let bar_type = resolution_to_bar_type(instrument_id, "60").unwrap();
1603
1604 assert_eq!(bar_type.instrument_id(), instrument_id);
1605 assert_eq!(bar_type.spec().step.get(), 60);
1606 assert_eq!(bar_type.spec().aggregation, BarAggregation::Minute);
1607 }
1608
1609 #[rstest]
1610 fn test_resolution_to_bar_type_daily() {
1611 let instrument_id = InstrumentId::from("BTC-PERPETUAL.DERIBIT");
1612 let bar_type = resolution_to_bar_type(instrument_id, "1D").unwrap();
1613
1614 assert_eq!(bar_type.instrument_id(), instrument_id);
1615 assert_eq!(bar_type.spec().step.get(), 1);
1616 assert_eq!(bar_type.spec().aggregation, BarAggregation::Day);
1617 }
1618
1619 #[rstest]
1620 fn test_resolution_to_bar_type_invalid() {
1621 let instrument_id = InstrumentId::from("BTC-PERPETUAL.DERIBIT");
1622 let result = resolution_to_bar_type(instrument_id, "invalid");
1623
1624 assert!(result.is_err());
1625 assert!(
1626 result
1627 .unwrap_err()
1628 .to_string()
1629 .contains("Unsupported Deribit resolution")
1630 );
1631 }
1632
1633 #[rstest]
1634 fn test_parse_chart_msg() {
1635 let instrument = test_perpetual_instrument();
1636 let json = load_test_json("ws_chart.json");
1637 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1638 let chart_msg: DeribitChartMsg =
1639 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1640
1641 assert_eq!(chart_msg.tick, 1_767_200_040_000);
1643 assert_eq!(chart_msg.open, 87490.0);
1644 assert_eq!(chart_msg.high, 87500.0);
1645 assert_eq!(chart_msg.low, 87465.0);
1646 assert_eq!(chart_msg.close, 87474.0);
1647 assert_eq!(chart_msg.volume, 0.95978896);
1648 assert_eq!(chart_msg.cost, 83970.0);
1649
1650 let bar_type = resolution_to_bar_type(instrument.id(), "1").unwrap();
1651
1652 let bar = parse_chart_msg(
1654 &chart_msg,
1655 bar_type,
1656 instrument.price_precision(),
1657 instrument.size_precision(),
1658 true,
1659 UnixNanos::default(),
1660 )
1661 .unwrap();
1662
1663 assert_eq!(bar.bar_type, bar_type);
1664 assert_eq!(bar.open, instrument.make_price(87490.0));
1665 assert_eq!(bar.high, instrument.make_price(87500.0));
1666 assert_eq!(bar.low, instrument.make_price(87465.0));
1667 assert_eq!(bar.close, instrument.make_price(87474.0));
1668 assert_eq!(bar.volume, instrument.make_qty(1.0, None)); assert_eq!(bar.ts_event, UnixNanos::new(1_767_200_100_000_000_000));
1672 }
1673
1674 #[rstest]
1675 fn test_parse_order_buy_response() {
1676 let instrument = test_perpetual_instrument();
1677 let json = load_test_json("ws_order_buy_response.json");
1678 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1679
1680 let order_msg: DeribitOrderMsg =
1682 serde_json::from_value(response["result"]["order"].clone()).unwrap();
1683
1684 assert_eq!(order_msg.order_id, "USDC-104819327443");
1686 assert_eq!(
1687 order_msg.label,
1688 Some("O-19700101-000000-001-001-1".to_string())
1689 );
1690 assert_eq!(order_msg.direction, "buy");
1691 assert_eq!(order_msg.order_state, "open");
1692 assert_eq!(order_msg.order_type, "limit");
1693 assert_eq!(order_msg.price, Some(dec!(2973.55)));
1694 assert_eq!(order_msg.amount, dec!(0.001));
1695 assert_eq!(order_msg.filled_amount, rust_decimal::Decimal::ZERO);
1696 assert!(order_msg.post_only);
1697 assert!(!order_msg.reduce_only);
1698
1699 let account_id = AccountId::new("DERIBIT-001");
1701 let trader_id = TraderId::new("TRADER-001");
1702 let strategy_id = StrategyId::new("PMM-001");
1703
1704 let accepted = parse_order_accepted(
1705 &order_msg,
1706 &instrument,
1707 account_id,
1708 trader_id,
1709 strategy_id,
1710 UnixNanos::default(),
1711 );
1712
1713 assert_eq!(
1714 accepted.client_order_id.to_string(),
1715 "O-19700101-000000-001-001-1"
1716 );
1717 assert_eq!(accepted.venue_order_id.to_string(), "USDC-104819327443");
1718 assert_eq!(accepted.trader_id, trader_id);
1719 assert_eq!(accepted.strategy_id, strategy_id);
1720 assert_eq!(accepted.account_id, account_id);
1721 }
1722
1723 #[rstest]
1724 fn test_parse_order_sell_response() {
1725 let instrument = test_perpetual_instrument();
1726 let json = load_test_json("ws_order_sell_response.json");
1727 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1728
1729 let order_msg: DeribitOrderMsg =
1730 serde_json::from_value(response["result"]["order"].clone()).unwrap();
1731
1732 assert_eq!(order_msg.order_id, "USDC-104819327458");
1734 assert_eq!(
1735 order_msg.label,
1736 Some("O-19700101-000000-001-001-2".to_string())
1737 );
1738 assert_eq!(order_msg.direction, "sell");
1739 assert_eq!(order_msg.order_state, "open");
1740 assert_eq!(order_msg.price, Some(dec!(3286.7)));
1741 assert_eq!(order_msg.amount, dec!(0.001));
1742
1743 let account_id = AccountId::new("DERIBIT-001");
1745 let trader_id = TraderId::new("TRADER-001");
1746 let strategy_id = StrategyId::new("PMM-001");
1747
1748 let accepted = parse_order_accepted(
1749 &order_msg,
1750 &instrument,
1751 account_id,
1752 trader_id,
1753 strategy_id,
1754 UnixNanos::default(),
1755 );
1756
1757 assert_eq!(
1758 accepted.client_order_id.to_string(),
1759 "O-19700101-000000-001-001-2"
1760 );
1761 assert_eq!(accepted.venue_order_id.to_string(), "USDC-104819327458");
1762 assert_eq!(accepted.trader_id, trader_id);
1763 assert_eq!(accepted.strategy_id, strategy_id);
1764 assert_eq!(accepted.account_id, account_id);
1765 }
1766
1767 #[rstest]
1768 fn test_parse_order_edit_response() {
1769 let instrument = test_perpetual_instrument();
1770 let json = load_test_json("ws_order_edit_response.json");
1771 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1772
1773 let order_msg: DeribitOrderMsg =
1774 serde_json::from_value(response["result"]["order"].clone()).unwrap();
1775
1776 assert_eq!(order_msg.order_id, "USDC-104819327443");
1778 assert_eq!(
1779 order_msg.label,
1780 Some("O-19700101-000000-001-001-1".to_string())
1781 );
1782 assert_eq!(order_msg.direction, "buy");
1783 assert_eq!(order_msg.order_state, "open");
1784 assert_eq!(order_msg.price, Some(dec!(3067.2))); let account_id = AccountId::new("DERIBIT-001");
1788 let trader_id = TraderId::new("TRADER-001");
1789 let strategy_id = StrategyId::new("PMM-001");
1790
1791 let updated = parse_order_updated(
1792 &order_msg,
1793 &instrument,
1794 account_id,
1795 trader_id,
1796 strategy_id,
1797 UnixNanos::default(),
1798 );
1799
1800 assert_eq!(
1801 updated.client_order_id.to_string(),
1802 "O-19700101-000000-001-001-1"
1803 );
1804 assert_eq!(
1805 updated.venue_order_id.unwrap().to_string(),
1806 "USDC-104819327443"
1807 );
1808 assert_eq!(updated.quantity.as_f64(), 0.0);
1810 }
1811
1812 #[rstest]
1813 fn test_parse_order_cancel_response() {
1814 let instrument = test_perpetual_instrument();
1815 let json = load_test_json("ws_order_cancel_response.json");
1816 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1817
1818 let order_msg: DeribitOrderMsg =
1820 serde_json::from_value(response["result"].clone()).unwrap();
1821
1822 assert_eq!(order_msg.order_id, "USDC-104819327443");
1824 assert_eq!(
1825 order_msg.label,
1826 Some("O-19700101-000000-001-001-1".to_string())
1827 );
1828 assert_eq!(order_msg.order_state, "cancelled");
1829 assert_eq!(order_msg.cancel_reason, Some("user_request".to_string()));
1830
1831 let account_id = AccountId::new("DERIBIT-001");
1833 let trader_id = TraderId::new("TRADER-001");
1834 let strategy_id = StrategyId::new("PMM-001");
1835
1836 let canceled = parse_order_canceled(
1837 &order_msg,
1838 &instrument,
1839 account_id,
1840 trader_id,
1841 strategy_id,
1842 UnixNanos::default(),
1843 );
1844
1845 assert_eq!(
1846 canceled.client_order_id.to_string(),
1847 "O-19700101-000000-001-001-1"
1848 );
1849 assert_eq!(
1850 canceled.venue_order_id.unwrap().to_string(),
1851 "USDC-104819327443"
1852 );
1853 assert_eq!(canceled.trader_id, trader_id);
1854 assert_eq!(canceled.strategy_id, strategy_id);
1855 }
1856
1857 #[rstest]
1858 fn test_parse_user_order_msg_to_status_report() {
1859 let instrument = test_perpetual_instrument();
1860 let json = load_test_json("ws_order_buy_response.json");
1861 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1862
1863 let order_msg: DeribitOrderMsg =
1864 serde_json::from_value(response["result"]["order"].clone()).unwrap();
1865
1866 let account_id = AccountId::new("DERIBIT-001");
1867 let report =
1868 parse_user_order_msg(&order_msg, &instrument, account_id, UnixNanos::default())
1869 .unwrap();
1870
1871 assert_eq!(report.venue_order_id.to_string(), "USDC-104819327443");
1872 assert_eq!(
1873 report.client_order_id.unwrap().to_string(),
1874 "O-19700101-000000-001-001-1"
1875 );
1876 assert_eq!(report.order_side, OrderSide::Buy);
1877 assert_eq!(report.order_type, OrderType::Limit);
1878 assert_eq!(report.time_in_force, TimeInForce::Gtc);
1879 assert_eq!(report.order_status, OrderStatus::Accepted);
1880 assert_eq!(report.quantity.as_f64(), 0.0);
1882 assert_eq!(report.filled_qty.as_f64(), 0.0);
1883 assert!(report.post_only);
1884 assert!(!report.reduce_only);
1885 }
1886
1887 #[rstest]
1888 fn test_determine_order_event_type() {
1889 assert_eq!(
1891 determine_order_event_type("open", true, false),
1892 OrderEventType::Accepted
1893 );
1894
1895 assert_eq!(
1897 determine_order_event_type("open", false, true),
1898 OrderEventType::Updated
1899 );
1900
1901 assert_eq!(
1903 determine_order_event_type("cancelled", false, false),
1904 OrderEventType::Canceled
1905 );
1906
1907 assert_eq!(
1909 determine_order_event_type("expired", false, false),
1910 OrderEventType::Expired
1911 );
1912
1913 assert_eq!(
1915 determine_order_event_type("filled", false, false),
1916 OrderEventType::None
1917 );
1918 }
1919}