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::{common::parse::build_public_trade_id, 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 = build_public_trade_id(
100 &msg.trade_id,
101 msg.block_rfq_id,
102 msg.block_trade_id.as_deref(),
103 msg.combo_id.as_deref(),
104 );
105 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
106
107 TradeTick::new_checked(
108 instrument_id,
109 price,
110 size,
111 aggressor_side,
112 trade_id,
113 ts_event,
114 ts_init,
115 )
116}
117
118pub fn parse_trades_data(
120 trades: &[DeribitTradeMsg],
121 instruments_cache: &AHashMap<Ustr, InstrumentAny>,
122 ts_init: UnixNanos,
123) -> Vec<Data> {
124 trades
125 .iter()
126 .filter_map(|msg| {
127 instruments_cache
128 .get(&msg.instrument_name)
129 .and_then(|inst| parse_trade_msg(msg, inst, ts_init).ok())
130 .map(Data::Trade)
131 })
132 .collect()
133}
134
135fn parse_snapshot_level(
136 level: &[serde_json::Value],
137 index: usize,
138 side: &str,
139 instrument_name: &str,
140) -> Option<(f64, f64)> {
141 let (price_val, amount_val) = if level.len() >= 3 {
142 let price = level[1].as_f64().or_else(|| {
143 log::warn!(
144 "Failed to parse {side} price at index {index} for {instrument_name}: {level:?}"
145 );
146 None
147 })?;
148 let amount = level[2].as_f64().or_else(|| {
149 log::warn!(
150 "Failed to parse {side} amount at index {index} for {instrument_name}: {level:?}"
151 );
152 None
153 })?;
154 (price, amount)
155 } else if level.len() >= 2 {
156 let price = level[0].as_f64().or_else(|| {
157 log::warn!(
158 "Failed to parse {side} price at index {index} for {instrument_name}: {level:?}"
159 );
160 None
161 })?;
162 let amount = level[1].as_f64().or_else(|| {
163 log::warn!(
164 "Failed to parse {side} amount at index {index} for {instrument_name}: {level:?}"
165 );
166 None
167 })?;
168 (price, amount)
169 } else {
170 log::warn!(
171 "Invalid {side} format at index {index} for {instrument_name}: expected 2-3 elements, was {}",
172 level.len()
173 );
174 return None;
175 };
176
177 if price_val <= 0.0 {
178 log::warn!(
179 "Invalid {side} price {price_val} at index {index} for {instrument_name}: {level:?}"
180 );
181 return None;
182 }
183
184 Some((price_val, amount_val))
185}
186
187fn parse_delta_level(
188 level: &[serde_json::Value],
189 index: usize,
190 side: &str,
191 instrument_name: &str,
192) -> Option<(BookAction, f64, f64)> {
193 if level.len() < 3 {
194 log::warn!(
195 "Invalid {side} delta format at index {index} for {instrument_name}: expected 3 elements, was {}",
196 level.len()
197 );
198 return None;
199 }
200
201 let action_str = level[0].as_str().or_else(|| {
202 log::warn!(
203 "Failed to parse {side} action at index {index} for {instrument_name}: {level:?}"
204 );
205 None
206 })?;
207
208 let deribit_action: DeribitBookAction = action_str.parse().ok().or_else(|| {
209 log::warn!(
210 "Unknown {side} action '{action_str}' at index {index} for {instrument_name}: {level:?}"
211 );
212 None
213 })?;
214
215 let price_val = level[1].as_f64().or_else(|| {
216 log::warn!(
217 "Failed to parse {side} price at index {index} for {instrument_name}: {level:?}"
218 );
219 None
220 })?;
221
222 let amount_val = level[2].as_f64().or_else(|| {
223 log::warn!(
224 "Failed to parse {side} amount at index {index} for {instrument_name}: {level:?}"
225 );
226 None
227 })?;
228
229 if price_val <= 0.0 {
230 log::warn!(
231 "Invalid {side} price {price_val} at index {index} for {instrument_name}: {level:?}"
232 );
233 return None;
234 }
235
236 Some((deribit_action.into(), price_val, amount_val))
237}
238
239pub fn parse_book_snapshot(
245 msg: &DeribitBookMsg,
246 instrument: &InstrumentAny,
247 ts_init: UnixNanos,
248) -> anyhow::Result<OrderBookDeltas> {
249 let instrument_id = instrument.id();
250 let price_precision = instrument.price_precision();
251 let size_precision = instrument.size_precision();
252 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
253
254 let mut deltas = Vec::new();
255
256 let has_levels = !msg.bids.is_empty() || !msg.asks.is_empty();
257
258 let clear_flags = if has_levels {
260 RecordFlag::F_SNAPSHOT as u8
261 } else {
262 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
263 };
264
265 deltas.push(OrderBookDelta::new(
266 instrument_id,
267 BookAction::Clear,
268 BookOrder::default(),
269 clear_flags,
270 msg.change_id,
271 ts_event,
272 ts_init,
273 ));
274
275 for (i, bid) in msg.bids.iter().enumerate() {
276 let Some((price_val, amount_val)) =
277 parse_snapshot_level(bid, i, "bid", msg.instrument_name.as_str())
278 else {
279 continue;
280 };
281
282 if amount_val > 0.0 {
283 let price = Price::new(price_val, price_precision);
284 let size = Quantity::new(amount_val, size_precision);
285
286 deltas.push(OrderBookDelta::new(
287 instrument_id,
288 BookAction::Add,
289 BookOrder::new(OrderSide::Buy, price, size, i as u64),
290 RecordFlag::F_SNAPSHOT as u8,
291 msg.change_id,
292 ts_event,
293 ts_init,
294 ));
295 }
296 }
297
298 let num_bids = msg.bids.len();
299 for (i, ask) in msg.asks.iter().enumerate() {
300 let Some((price_val, amount_val)) =
301 parse_snapshot_level(ask, i, "ask", msg.instrument_name.as_str())
302 else {
303 continue;
304 };
305
306 if amount_val > 0.0 {
307 let price = Price::new(price_val, price_precision);
308 let size = Quantity::new(amount_val, size_precision);
309
310 deltas.push(OrderBookDelta::new(
311 instrument_id,
312 BookAction::Add,
313 BookOrder::new(OrderSide::Sell, price, size, (num_bids + i) as u64),
314 RecordFlag::F_SNAPSHOT as u8,
315 msg.change_id,
316 ts_event,
317 ts_init,
318 ));
319 }
320 }
321
322 if let Some(last) = deltas.last_mut() {
323 *last = OrderBookDelta::new(
324 last.instrument_id,
325 last.action,
326 last.order,
327 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8,
328 last.sequence,
329 last.ts_event,
330 last.ts_init,
331 );
332 }
333
334 Ok(OrderBookDeltas::new(instrument_id, deltas))
335}
336
337pub fn parse_book_delta(
343 msg: &DeribitBookMsg,
344 instrument: &InstrumentAny,
345 ts_init: UnixNanos,
346) -> anyhow::Result<OrderBookDeltas> {
347 let instrument_id = instrument.id();
348 let price_precision = instrument.price_precision();
349 let size_precision = instrument.size_precision();
350 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
351
352 let mut deltas = Vec::new();
353
354 for (i, bid) in msg.bids.iter().enumerate() {
355 let Some((action, price_val, amount_val)) =
356 parse_delta_level(bid, i, "bid", msg.instrument_name.as_str())
357 else {
358 continue;
359 };
360
361 let price = Price::new(price_val, price_precision);
362 let size = Quantity::new(amount_val.abs(), size_precision);
363
364 deltas.push(OrderBookDelta::new(
365 instrument_id,
366 action,
367 BookOrder::new(OrderSide::Buy, price, size, i as u64),
368 0,
369 msg.change_id,
370 ts_event,
371 ts_init,
372 ));
373 }
374
375 let num_bids = msg.bids.len();
376 for (i, ask) in msg.asks.iter().enumerate() {
377 let Some((action, price_val, amount_val)) =
378 parse_delta_level(ask, i, "ask", msg.instrument_name.as_str())
379 else {
380 continue;
381 };
382
383 let price = Price::new(price_val, price_precision);
384 let size = Quantity::new(amount_val.abs(), size_precision);
385
386 deltas.push(OrderBookDelta::new(
387 instrument_id,
388 action,
389 BookOrder::new(OrderSide::Sell, price, size, (num_bids + i) as u64),
390 0,
391 msg.change_id,
392 ts_event,
393 ts_init,
394 ));
395 }
396
397 if let Some(last) = deltas.last_mut() {
399 *last = OrderBookDelta::new(
400 last.instrument_id,
401 last.action,
402 last.order,
403 RecordFlag::F_LAST as u8,
404 last.sequence,
405 last.ts_event,
406 last.ts_init,
407 );
408 }
409
410 Ok(OrderBookDeltas::new(instrument_id, deltas))
411}
412
413pub fn parse_book_msg(
419 msg: &DeribitBookMsg,
420 instrument: &InstrumentAny,
421 ts_init: UnixNanos,
422) -> anyhow::Result<OrderBookDeltas> {
423 match msg.msg_type {
424 DeribitBookMsgType::Snapshot => parse_book_snapshot(msg, instrument, ts_init),
425 DeribitBookMsgType::Change => parse_book_delta(msg, instrument, ts_init),
426 }
427}
428
429pub fn parse_ticker_to_quote(
435 msg: &DeribitTickerMsg,
436 instrument: &InstrumentAny,
437 ts_init: UnixNanos,
438) -> anyhow::Result<QuoteTick> {
439 let instrument_id = instrument.id();
440 let price_precision = instrument.price_precision();
441 let size_precision = instrument.size_precision();
442
443 let bid_price_val = msg
444 .best_bid_price
445 .context("Missing best_bid_price in ticker")?;
446 let ask_price_val = msg
447 .best_ask_price
448 .context("Missing best_ask_price in ticker")?;
449
450 let bid_price = Price::from_decimal_dp(bid_price_val, price_precision)?;
451 let ask_price = Price::from_decimal_dp(ask_price_val, price_precision)?;
452 let bid_size =
453 Quantity::from_decimal_dp(msg.best_bid_amount.unwrap_or_default(), size_precision)?;
454 let ask_size =
455 Quantity::from_decimal_dp(msg.best_ask_amount.unwrap_or_default(), size_precision)?;
456 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
457
458 QuoteTick::new_checked(
459 instrument_id,
460 bid_price,
461 ask_price,
462 bid_size,
463 ask_size,
464 ts_event,
465 ts_init,
466 )
467}
468
469pub fn parse_quote_msg(
475 msg: &DeribitQuoteMsg,
476 instrument: &InstrumentAny,
477 ts_init: UnixNanos,
478) -> anyhow::Result<QuoteTick> {
479 let instrument_id = instrument.id();
480 let price_precision = instrument.price_precision();
481 let size_precision = instrument.size_precision();
482
483 let bid_price = Price::from_decimal_dp(msg.best_bid_price, price_precision)?;
484 let ask_price = Price::from_decimal_dp(msg.best_ask_price, price_precision)?;
485 let bid_size = Quantity::from_decimal_dp(msg.best_bid_amount, size_precision)?;
486 let ask_size = Quantity::from_decimal_dp(msg.best_ask_amount, size_precision)?;
487 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
488
489 QuoteTick::new_checked(
490 instrument_id,
491 bid_price,
492 ask_price,
493 bid_size,
494 ask_size,
495 ts_event,
496 ts_init,
497 )
498}
499
500pub fn parse_ticker_to_mark_price(
506 msg: &DeribitTickerMsg,
507 instrument: &InstrumentAny,
508 ts_init: UnixNanos,
509) -> anyhow::Result<MarkPriceUpdate> {
510 let instrument_id = instrument.id();
511 let price_precision = instrument.price_precision();
512 let value = Price::from_decimal_dp(msg.mark_price, price_precision)?;
513 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
514
515 Ok(MarkPriceUpdate::new(
516 instrument_id,
517 value,
518 ts_event,
519 ts_init,
520 ))
521}
522
523pub fn parse_ticker_to_index_price(
529 msg: &DeribitTickerMsg,
530 instrument: &InstrumentAny,
531 ts_init: UnixNanos,
532) -> anyhow::Result<IndexPriceUpdate> {
533 let instrument_id = instrument.id();
534 let price_precision = instrument.price_precision();
535 let value = Price::from_decimal_dp(msg.index_price, price_precision)?;
536 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
537
538 Ok(IndexPriceUpdate::new(
539 instrument_id,
540 value,
541 ts_event,
542 ts_init,
543 ))
544}
545
546#[must_use]
550pub fn parse_ticker_to_funding_rate(
551 msg: &DeribitTickerMsg,
552 instrument: &InstrumentAny,
553 ts_init: UnixNanos,
554) -> Option<FundingRateUpdate> {
555 let rate = msg.current_funding?;
557 let instrument_id = instrument.id();
558 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
559
560 Some(FundingRateUpdate::new(
562 instrument_id,
563 rate,
564 None, None, ts_event,
567 ts_init,
568 ))
569}
570
571#[must_use]
575pub fn parse_ticker_to_option_greeks(
576 msg: &DeribitTickerMsg,
577 instrument: &InstrumentAny,
578 ts_init: UnixNanos,
579) -> Option<OptionGreeks> {
580 let deribit_greeks = msg.greeks.as_ref()?;
581 let instrument_id = instrument.id();
582 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
583
584 Some(OptionGreeks {
585 instrument_id,
586 convention: GreeksConvention::BlackScholes,
587 greeks: deribit_greeks.to_greek_values(),
588 mark_iv: msg.mark_iv.and_then(|v| v.to_f64()),
589 bid_iv: msg.bid_iv.and_then(|v| v.to_f64()),
590 ask_iv: msg.ask_iv.and_then(|v| v.to_f64()),
591 underlying_price: msg.underlying_price.and_then(|v| v.to_f64()),
592 open_interest: Some(msg.open_interest.to_f64().unwrap_or(0.0)),
593 ts_event,
594 ts_init,
595 })
596}
597
598#[must_use]
603pub fn parse_perpetual_to_funding_rate(
604 msg: &DeribitPerpetualMsg,
605 instrument: &InstrumentAny,
606 ts_init: UnixNanos,
607) -> FundingRateUpdate {
608 let instrument_id = instrument.id();
609 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
610
611 FundingRateUpdate::new(
612 instrument_id,
613 msg.interest,
614 None, None, ts_event,
617 ts_init,
618 )
619}
620
621pub fn resolution_to_bar_type(
629 instrument_id: InstrumentId,
630 resolution: &str,
631) -> anyhow::Result<BarType> {
632 let (step, aggregation) = match resolution {
633 "1" => (1, BarAggregation::Minute),
634 "3" => (3, BarAggregation::Minute),
635 "5" => (5, BarAggregation::Minute),
636 "10" => (10, BarAggregation::Minute),
637 "15" => (15, BarAggregation::Minute),
638 "30" => (30, BarAggregation::Minute),
639 "60" => (1, BarAggregation::Hour),
640 "120" => (2, BarAggregation::Hour),
641 "180" => (3, BarAggregation::Hour),
642 "360" => (6, BarAggregation::Hour),
643 "720" => (12, BarAggregation::Hour),
644 "1D" => (1, BarAggregation::Day),
645 _ => anyhow::bail!("Unsupported Deribit resolution: {resolution}"),
646 };
647
648 let spec = BarSpecification::new_checked(step, aggregation, PriceType::Last)
649 .context("invalid Deribit bar resolution")?;
650 Ok(BarType::new(
651 instrument_id,
652 spec,
653 AggregationSource::External,
654 ))
655}
656
657pub fn parse_chart_msg(
673 chart_msg: &DeribitChartMsg,
674 bar_type: BarType,
675 price_precision: u8,
676 size_precision: u8,
677 use_cost_for_volume: bool,
678 timestamp_on_close: bool,
679 ts_init: UnixNanos,
680) -> anyhow::Result<Bar> {
681 let open = Price::new_checked(chart_msg.open, price_precision).context("Invalid open price")?;
682 let high = Price::new_checked(chart_msg.high, price_precision).context("Invalid high price")?;
683 let low = Price::new_checked(chart_msg.low, price_precision).context("Invalid low price")?;
684 let close =
685 Price::new_checked(chart_msg.close, price_precision).context("Invalid close price")?;
686 let raw_volume = if use_cost_for_volume {
687 chart_msg.cost
688 } else {
689 chart_msg.volume
690 };
691 let volume = Quantity::new_checked(raw_volume, size_precision).context("Invalid volume")?;
692
693 let mut ts_event = UnixNanos::from(chart_msg.tick * NANOSECONDS_IN_MILLISECOND);
695
696 if timestamp_on_close {
698 let interval_ns = bar_type
699 .spec()
700 .timedelta()
701 .num_nanoseconds()
702 .context("bar specification produced non-integer interval")?;
703 let interval_ns = u64::try_from(interval_ns)
704 .context("bar interval overflowed the u64 range for nanoseconds")?;
705 let updated = ts_event
706 .as_u64()
707 .checked_add(interval_ns)
708 .context("bar timestamp overflowed when adjusting to close time")?;
709 ts_event = UnixNanos::from(updated);
710 }
711
712 Bar::new_checked(bar_type, open, high, low, close, volume, ts_event, ts_init)
713 .context("Invalid OHLC bar")
714}
715
716pub fn parse_user_order_msg(
722 msg: &DeribitOrderMsg,
723 instrument: &InstrumentAny,
724 account_id: AccountId,
725 ts_init: UnixNanos,
726) -> anyhow::Result<OrderStatusReport> {
727 let instrument_id = instrument.id();
728 let venue_order_id = VenueOrderId::new(&msg.order_id);
729
730 let order_side = match msg.direction.as_str() {
731 "buy" => OrderSide::Buy,
732 "sell" => OrderSide::Sell,
733 _ => anyhow::bail!("Unknown order direction: {}", msg.direction),
734 };
735
736 let order_type = match msg.order_type.as_str() {
738 "limit" => OrderType::Limit,
739 "market" => OrderType::Market,
740 "stop_limit" => OrderType::StopLimit,
741 "stop_market" => OrderType::StopMarket,
742 "take_limit" => OrderType::LimitIfTouched,
743 "take_market" => OrderType::MarketIfTouched,
744 other => {
745 log::warn!("Unknown Deribit order_type '{other}', defaulting to Limit");
746 OrderType::Limit
747 }
748 };
749
750 let time_in_force = match msg.time_in_force.as_str() {
752 "good_til_cancelled" => TimeInForce::Gtc,
753 "good_til_day" => TimeInForce::Gtd,
754 "fill_or_kill" => TimeInForce::Fok,
755 "immediate_or_cancel" => TimeInForce::Ioc,
756 other => {
757 log::warn!("Unknown time_in_force '{other}', defaulting to GTC");
758 TimeInForce::Gtc
759 }
760 };
761
762 let order_status = match msg.order_state.as_str() {
764 "open" => {
765 if msg.filled_amount.is_zero() {
766 OrderStatus::Accepted
767 } else {
768 OrderStatus::PartiallyFilled
769 }
770 }
771 "filled" => OrderStatus::Filled,
772 "rejected" => OrderStatus::Rejected,
773 "cancelled" => OrderStatus::Canceled,
774 "untriggered" => OrderStatus::Accepted, other => {
776 log::warn!("Unknown Deribit order_state '{other}', defaulting to Accepted");
777 OrderStatus::Accepted
778 }
779 };
780
781 let price_precision = instrument.price_precision();
782 let size_precision = instrument.size_precision();
783
784 let quantity = Quantity::from_decimal_dp(msg.amount, size_precision)?;
785 let filled_qty = Quantity::from_decimal_dp(msg.filled_amount, size_precision)?;
786
787 let ts_accepted = UnixNanos::new(msg.creation_timestamp * NANOSECONDS_IN_MILLISECOND);
788 let ts_last = UnixNanos::new(msg.last_update_timestamp * NANOSECONDS_IN_MILLISECOND);
789
790 let mut report = OrderStatusReport::new(
791 account_id,
792 instrument_id,
793 None, venue_order_id,
795 order_side,
796 order_type,
797 time_in_force,
798 order_status,
799 quantity,
800 filled_qty,
801 ts_accepted,
802 ts_last,
803 ts_init,
804 Some(UUID4::new()),
805 );
806
807 if let Some(ref label) = msg.label
809 && !label.is_empty()
810 {
811 report = report.with_client_order_id(ClientOrderId::new(label));
812 }
813
814 if let Some(price_val) = msg.price
816 && !price_val.is_zero()
817 {
818 let price = Price::from_decimal_dp(price_val, price_precision)?;
819 report = report.with_price(price);
820 }
821
822 if time_in_force == TimeInForce::Gtd {
823 let expire_time = next_8_utc(ts_accepted)?;
824 report = report.with_expire_time(expire_time);
825 }
826
827 if let Some(avg_price) = msg.average_price
829 && !avg_price.is_zero()
830 {
831 report = report.with_avg_px(avg_price.to_f64().unwrap_or_default())?;
832 }
833
834 if let Some(trigger_price) = msg.trigger_price
836 && !trigger_price.is_zero()
837 {
838 let trigger = Price::from_decimal_dp(trigger_price, price_precision)?;
839 report = report.with_trigger_price(trigger);
840 }
841
842 if msg.post_only {
843 report = report.with_post_only(true);
844 }
845
846 if msg.reduce_only {
847 report = report.with_reduce_only(true);
848 }
849
850 if let Some(ref reason) = msg.reject_reason {
852 report = report.with_cancel_reason(reason.clone());
853 } else if let Some(ref reason) = msg.cancel_reason {
854 report = report.with_cancel_reason(reason.clone());
855 }
856
857 Ok(report)
858}
859
860pub fn parse_user_trade_msg(
866 msg: &DeribitUserTradeMsg,
867 instrument: &InstrumentAny,
868 account_id: AccountId,
869 ts_init: UnixNanos,
870) -> anyhow::Result<FillReport> {
871 let instrument_id = instrument.id();
872 let venue_order_id = VenueOrderId::new(&msg.order_id);
873 let trade_id = TradeId::new(&msg.trade_id);
874
875 if let Some(liq) = msg.liquidation.as_deref().filter(|s| !s.is_empty()) {
878 let who = match liq {
879 "M" => "maker",
880 "T" => "taker",
881 "MT" => "both",
882 _ => liq,
883 };
884 log::warn!(
885 "Liquidation trade: {} trade_id={} order_id={} liquidation_side={} direction={} amount={} price={}",
886 instrument_id,
887 msg.trade_id,
888 msg.order_id,
889 who,
890 msg.direction,
891 msg.amount,
892 msg.price,
893 );
894 }
895
896 let order_side = match msg.direction.as_str() {
897 "buy" => OrderSide::Buy,
898 "sell" => OrderSide::Sell,
899 _ => anyhow::bail!("Unknown trade direction: {}", msg.direction),
900 };
901
902 let price_precision = instrument.price_precision();
903 let size_precision = instrument.size_precision();
904
905 let last_qty = Quantity::from_decimal_dp(msg.amount, size_precision)?;
906 let last_px = Price::from_decimal_dp(msg.price, price_precision)?;
907
908 let liquidity_side = match msg.liquidity.as_str() {
909 "M" => LiquiditySide::Maker,
910 "T" => LiquiditySide::Taker,
911 _ => LiquiditySide::NoLiquiditySide,
912 };
913
914 let fee_currency = Currency::from(&msg.fee_currency);
916 let commission = Money::from_decimal(msg.fee, fee_currency)?;
917
918 let ts_event = UnixNanos::new(msg.timestamp * NANOSECONDS_IN_MILLISECOND);
919
920 let client_order_id = msg
921 .label
922 .as_ref()
923 .filter(|l| !l.is_empty())
924 .map(ClientOrderId::new);
925
926 Ok(FillReport::new(
927 account_id,
928 instrument_id,
929 venue_order_id,
930 trade_id,
931 order_side,
932 last_qty,
933 last_px,
934 commission,
935 liquidity_side,
936 client_order_id,
937 None, ts_event,
939 ts_init,
940 None, ))
942}
943
944#[must_use]
955pub fn parse_position_status_report(
956 position: &DeribitPosition,
957 instrument: &InstrumentAny,
958 account_id: AccountId,
959 ts_init: UnixNanos,
960) -> PositionStatusReport {
961 let instrument_id = instrument.id();
962 let size_precision = instrument.size_precision();
963
964 let signed_qty = Quantity::from_decimal_dp(position.size.abs(), size_precision)
965 .unwrap_or_else(|_| Quantity::zero(size_precision));
966
967 let position_side = match position.direction.as_str() {
968 "buy" => PositionSideSpecified::Long,
969 "sell" => PositionSideSpecified::Short,
970 _ => PositionSideSpecified::Flat,
971 };
972
973 let avg_px_open = Some(position.average_price);
975
976 PositionStatusReport::new(
977 account_id,
978 instrument_id,
979 position_side,
980 signed_qty,
981 ts_init,
982 ts_init,
983 Some(UUID4::new()),
984 None, avg_px_open,
986 )
987}
988
989#[derive(Debug, Clone)]
994pub enum ParsedOrderEvent {
995 Accepted(OrderAccepted),
997 Canceled(OrderCanceled),
999 Expired(OrderExpired),
1001 Updated(OrderUpdated),
1003 None,
1005}
1006
1007fn extract_client_order_id(msg: &DeribitOrderMsg) -> Option<ClientOrderId> {
1009 msg.label
1010 .as_ref()
1011 .filter(|l| !l.is_empty())
1012 .map(ClientOrderId::new)
1013}
1014
1015#[must_use]
1020pub fn parse_order_accepted(
1021 msg: &DeribitOrderMsg,
1022 instrument: &InstrumentAny,
1023 account_id: AccountId,
1024 trader_id: TraderId,
1025 strategy_id: StrategyId,
1026 ts_init: UnixNanos,
1027) -> OrderAccepted {
1028 let instrument_id = instrument.id();
1029 let venue_order_id = VenueOrderId::new(&msg.order_id);
1030 let client_order_id = extract_client_order_id(msg).unwrap_or_else(|| {
1031 ClientOrderId::new(&msg.order_id)
1033 });
1034 let ts_event = UnixNanos::new(msg.last_update_timestamp * NANOSECONDS_IN_MILLISECOND);
1035
1036 OrderAccepted::new(
1037 trader_id,
1038 strategy_id,
1039 instrument_id,
1040 client_order_id,
1041 venue_order_id,
1042 account_id,
1043 nautilus_core::UUID4::new(),
1044 ts_event,
1045 ts_init,
1046 false, )
1048}
1049
1050#[must_use]
1054pub fn parse_order_canceled(
1055 msg: &DeribitOrderMsg,
1056 instrument: &InstrumentAny,
1057 account_id: AccountId,
1058 trader_id: TraderId,
1059 strategy_id: StrategyId,
1060 ts_init: UnixNanos,
1061) -> OrderCanceled {
1062 let instrument_id = instrument.id();
1063 let venue_order_id = VenueOrderId::new(&msg.order_id);
1064 let client_order_id =
1065 extract_client_order_id(msg).unwrap_or_else(|| ClientOrderId::new(&msg.order_id));
1066 let ts_event = UnixNanos::new(msg.last_update_timestamp * NANOSECONDS_IN_MILLISECOND);
1067
1068 OrderCanceled::new(
1069 trader_id,
1070 strategy_id,
1071 instrument_id,
1072 client_order_id,
1073 nautilus_core::UUID4::new(),
1074 ts_event,
1075 ts_init,
1076 false, Some(venue_order_id),
1078 Some(account_id),
1079 )
1080}
1081
1082#[must_use]
1087pub fn parse_order_expired(
1088 msg: &DeribitOrderMsg,
1089 instrument: &InstrumentAny,
1090 account_id: AccountId,
1091 trader_id: TraderId,
1092 strategy_id: StrategyId,
1093 ts_init: UnixNanos,
1094) -> OrderExpired {
1095 let instrument_id = instrument.id();
1096 let venue_order_id = VenueOrderId::new(&msg.order_id);
1097 let client_order_id =
1098 extract_client_order_id(msg).unwrap_or_else(|| ClientOrderId::new(&msg.order_id));
1099 let ts_event = UnixNanos::new(msg.last_update_timestamp * NANOSECONDS_IN_MILLISECOND);
1100
1101 OrderExpired::new(
1102 trader_id,
1103 strategy_id,
1104 instrument_id,
1105 client_order_id,
1106 nautilus_core::UUID4::new(),
1107 ts_event,
1108 ts_init,
1109 false, Some(venue_order_id),
1111 Some(account_id),
1112 )
1113}
1114
1115#[must_use]
1119pub fn parse_order_updated(
1120 msg: &DeribitOrderMsg,
1121 instrument: &InstrumentAny,
1122 account_id: AccountId,
1123 trader_id: TraderId,
1124 strategy_id: StrategyId,
1125 ts_init: UnixNanos,
1126) -> OrderUpdated {
1127 let instrument_id = instrument.id();
1128 let price_precision = instrument.price_precision();
1129 let size_precision = instrument.size_precision();
1130
1131 let venue_order_id = VenueOrderId::new(&msg.order_id);
1132 let client_order_id =
1133 extract_client_order_id(msg).unwrap_or_else(|| ClientOrderId::new(&msg.order_id));
1134 let quantity = Quantity::from_decimal_dp(msg.amount, size_precision)
1135 .unwrap_or_else(|_| Quantity::zero(size_precision));
1136 let price = msg
1137 .price
1138 .and_then(|p| Price::from_decimal_dp(p, price_precision).ok());
1139 let trigger_price = msg
1140 .trigger_price
1141 .and_then(|p| Price::from_decimal_dp(p, price_precision).ok());
1142 let ts_event = UnixNanos::new(msg.last_update_timestamp * NANOSECONDS_IN_MILLISECOND);
1143
1144 OrderUpdated::new(
1145 trader_id,
1146 strategy_id,
1147 instrument_id,
1148 client_order_id,
1149 quantity,
1150 nautilus_core::UUID4::new(),
1151 ts_event,
1152 ts_init,
1153 false, Some(venue_order_id),
1155 Some(account_id),
1156 price,
1157 trigger_price,
1158 None, false, )
1161}
1162
1163#[must_use]
1176pub fn determine_order_event_type(
1177 order_state: &str,
1178 is_new_order: bool,
1179 was_amended: bool,
1180) -> OrderEventType {
1181 match order_state {
1182 "open" | "untriggered" => {
1183 if was_amended {
1184 OrderEventType::Updated
1185 } else if is_new_order {
1186 OrderEventType::Accepted
1187 } else {
1188 OrderEventType::None
1190 }
1191 }
1192 "cancelled" => OrderEventType::Canceled,
1193 "expired" => OrderEventType::Expired,
1194 "filled" => {
1195 OrderEventType::None
1197 }
1198 "rejected" => {
1199 OrderEventType::None
1201 }
1202 other => {
1203 log::warn!("Unknown Deribit order_state '{other}' in event routing, dropping");
1204 OrderEventType::None
1205 }
1206 }
1207}
1208
1209#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1211pub enum OrderEventType {
1212 Accepted,
1214 Canceled,
1216 Expired,
1218 Updated,
1220 None,
1222}
1223
1224#[cfg(test)]
1225mod tests {
1226 use rstest::rstest;
1227 use rust_decimal_macros::dec;
1228
1229 use super::*;
1230 use crate::{
1231 common::{parse::parse_deribit_instrument_any, testing::load_test_json},
1232 http::models::{DeribitInstrument, DeribitJsonRpcResponse},
1233 };
1234
1235 fn test_perpetual_instrument() -> InstrumentAny {
1237 let json = load_test_json("http_get_instruments.json");
1238 let response: DeribitJsonRpcResponse<Vec<DeribitInstrument>> =
1239 serde_json::from_str(&json).unwrap();
1240 let instrument = &response.result.unwrap()[0];
1241 parse_deribit_instrument_any(instrument, UnixNanos::default(), UnixNanos::default())
1242 .unwrap()
1243 .unwrap()
1244 }
1245
1246 #[rstest]
1247 fn test_parse_trade_msg_sell() {
1248 let instrument = test_perpetual_instrument();
1249 let json = load_test_json("ws_trades.json");
1250 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1251 let trades: Vec<DeribitTradeMsg> =
1252 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1253 let msg = &trades[0];
1254
1255 let tick = parse_trade_msg(msg, &instrument, UnixNanos::default()).unwrap();
1256
1257 assert_eq!(tick.instrument_id, instrument.id());
1258 assert_eq!(tick.price, instrument.make_price(92294.5));
1259 assert_eq!(tick.size, instrument.make_qty(10.0, None));
1260 assert_eq!(tick.aggressor_side, AggressorSide::Seller);
1261 assert_eq!(tick.trade_id.to_string(), "403691824");
1262 assert_eq!(tick.ts_event, UnixNanos::new(1_765_531_356_452_000_000));
1263 }
1264
1265 #[rstest]
1266 fn test_parse_trade_msg_buy() {
1267 let instrument = test_perpetual_instrument();
1268 let json = load_test_json("ws_trades.json");
1269 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1270 let trades: Vec<DeribitTradeMsg> =
1271 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1272 let msg = &trades[1];
1273
1274 let tick = parse_trade_msg(msg, &instrument, UnixNanos::default()).unwrap();
1275
1276 assert_eq!(tick.instrument_id, instrument.id());
1277 assert_eq!(tick.price, instrument.make_price(92288.5));
1278 assert_eq!(tick.size, instrument.make_qty(750.0, None));
1279 assert_eq!(tick.aggressor_side, AggressorSide::Seller);
1280 assert_eq!(tick.trade_id.to_string(), "403691825");
1281 }
1282
1283 fn make_trade_msg(
1284 instrument_name: &str,
1285 trade_id: &str,
1286 block_trade_id: Option<&str>,
1287 block_rfq_id: Option<i64>,
1288 combo_id: Option<&str>,
1289 ) -> DeribitTradeMsg {
1290 let raw = serde_json::json!({
1291 "trade_id": trade_id,
1292 "instrument_name": instrument_name,
1293 "price": 92294.5,
1294 "amount": 10.0,
1295 "direction": "buy",
1296 "timestamp": 1_765_531_356_452_u64,
1297 "trade_seq": 1,
1298 "tick_direction": 0,
1299 "index_price": 92276.75,
1300 "mark_price": 92287.11,
1301 "block_trade_id": block_trade_id,
1302 "block_rfq_id": block_rfq_id,
1303 "combo_id": combo_id,
1304 });
1305 serde_json::from_value(raw).unwrap()
1306 }
1307
1308 #[rstest]
1309 fn test_parse_trade_msg_tags_block_trade() {
1310 let instrument = test_perpetual_instrument();
1311 let msg = make_trade_msg("BTC-PERPETUAL", "244343055", Some("12345"), None, None);
1312 let tick = parse_trade_msg(&msg, &instrument, UnixNanos::default()).unwrap();
1313 assert_eq!(tick.trade_id.to_string(), "BLK-244343055");
1314 }
1315
1316 #[rstest]
1317 fn test_parse_trade_msg_tags_block_rfq() {
1318 let instrument = test_perpetual_instrument();
1319 let msg = make_trade_msg("BTC-PERPETUAL", "244343055", None, Some(99), None);
1320 let tick = parse_trade_msg(&msg, &instrument, UnixNanos::default()).unwrap();
1321 assert_eq!(tick.trade_id.to_string(), "RFQ-244343055");
1322 }
1323
1324 #[rstest]
1325 fn test_parse_trade_msg_tags_combo_leg() {
1326 let instrument = test_perpetual_instrument();
1330 let msg = make_trade_msg(
1331 "BTC-PERPETUAL",
1332 "244343055",
1333 None,
1334 None,
1335 Some("BTC-FS-25DEC26_PERP"),
1336 );
1337 let tick = parse_trade_msg(&msg, &instrument, UnixNanos::default()).unwrap();
1338 assert_eq!(tick.trade_id.to_string(), "COMBO-244343055");
1339 }
1340
1341 fn load_combo_option_instrument() -> InstrumentAny {
1342 let combo_json = load_test_json("http_get_instruments_option_combo.json");
1343 let combo_response: DeribitJsonRpcResponse<Vec<DeribitInstrument>> =
1344 serde_json::from_str(&combo_json).unwrap();
1345 let combo_raw = combo_response
1346 .result
1347 .unwrap()
1348 .into_iter()
1349 .find(|i| i.instrument_name.as_str() == "BTC-CS-19MAY26-70000_75000")
1350 .expect("fixture must contain BTC-CS-19MAY26-70000_75000");
1351 parse_deribit_instrument_any(&combo_raw, UnixNanos::default(), UnixNanos::default())
1352 .unwrap()
1353 .unwrap()
1354 }
1355
1356 fn load_combo_trade_msgs() -> Vec<DeribitTradeMsg> {
1357 let json = load_test_json("ws_trades_option_combo.json");
1358 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1359 serde_json::from_value(response["params"]["data"].clone()).unwrap()
1360 }
1361
1362 #[rstest]
1363 fn test_parse_trades_data_combo_emits_single_tick() {
1364 let combo_inst = load_combo_option_instrument();
1368 let mut cache: AHashMap<Ustr, InstrumentAny> = AHashMap::new();
1369 cache.insert(Ustr::from("BTC-CS-19MAY26-70000_75000"), combo_inst.clone());
1370
1371 let trades = load_combo_trade_msgs();
1372 let legs = trades[0].legs.as_ref().expect("combo trade must have legs");
1374 assert_eq!(legs.len(), 2);
1375
1376 let data = parse_trades_data(&trades, &cache, UnixNanos::default());
1377 assert_eq!(data.len(), 1, "should emit one tick for the combo only");
1378
1379 let Data::Trade(tick) = &data[0] else {
1380 panic!("expected Data::Trade");
1381 };
1382 assert_eq!(tick.instrument_id, combo_inst.id());
1383 assert_eq!(tick.price, Price::from("0.0639"));
1386 assert_eq!(tick.size, Quantity::from("0.1"));
1387 assert_eq!(tick.aggressor_side, AggressorSide::Seller);
1388 assert_eq!(tick.trade_id.to_string(), "244365193");
1389 }
1390
1391 #[rstest]
1392 fn test_parse_trades_data_combo_not_cached_emits_no_tick() {
1393 let cache: AHashMap<Ustr, InstrumentAny> = AHashMap::new();
1397 let trades = load_combo_trade_msgs();
1398 let data = parse_trades_data(&trades, &cache, UnixNanos::default());
1399 assert!(data.is_empty(), "uncached combo must not emit ticks");
1400 }
1401
1402 #[rstest]
1403 fn test_parse_trades_data_mixed_combo_and_per_leg() {
1404 let combo_inst = load_combo_option_instrument();
1407 let perp_inst = test_perpetual_instrument();
1408
1409 let mut cache: AHashMap<Ustr, InstrumentAny> = AHashMap::new();
1410 cache.insert(Ustr::from("BTC-CS-19MAY26-70000_75000"), combo_inst.clone());
1411 cache.insert(Ustr::from("BTC-PERPETUAL"), perp_inst.clone());
1412
1413 let mut trades = load_combo_trade_msgs();
1414 let perp_msgs: Vec<DeribitTradeMsg> = {
1415 let json = load_test_json("ws_trades.json");
1416 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1417 serde_json::from_value(response["params"]["data"].clone()).unwrap()
1418 };
1419 trades.extend(perp_msgs);
1420
1421 let data = parse_trades_data(&trades, &cache, UnixNanos::default());
1422 assert_eq!(data.len(), 3);
1424
1425 let mut combo_ticks = 0;
1426 let mut perp_ticks = 0;
1427
1428 for item in &data {
1429 let Data::Trade(tick) = item else {
1430 panic!("expected Data::Trade, was {item:?}");
1431 };
1432
1433 if tick.instrument_id == combo_inst.id() {
1434 combo_ticks += 1;
1435 assert_eq!(tick.trade_id.to_string(), "244365193");
1436 } else if tick.instrument_id == perp_inst.id() {
1437 perp_ticks += 1;
1438 } else {
1439 panic!("unexpected instrument_id: {}", tick.instrument_id);
1440 }
1441 }
1442 assert_eq!(combo_ticks, 1);
1443 assert_eq!(perp_ticks, 2);
1444 }
1445
1446 #[rstest]
1447 fn test_parse_book_snapshot() {
1448 let instrument = test_perpetual_instrument();
1449 let json = load_test_json("ws_book_snapshot.json");
1450 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1451 let msg: DeribitBookMsg =
1452 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1453
1454 let deltas = parse_book_snapshot(&msg, &instrument, UnixNanos::default()).unwrap();
1455
1456 assert_eq!(deltas.instrument_id, instrument.id());
1457 assert_eq!(deltas.deltas.len(), 11);
1459
1460 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
1462
1463 let first_bid = &deltas.deltas[1];
1465 assert_eq!(first_bid.action, BookAction::Add);
1466 assert_eq!(first_bid.order.side, OrderSide::Buy);
1467 assert_eq!(first_bid.order.price, instrument.make_price(42500.0));
1468 assert_eq!(first_bid.order.size, instrument.make_qty(1000.0, None));
1469
1470 let first_ask = &deltas.deltas[6];
1472 assert_eq!(first_ask.action, BookAction::Add);
1473 assert_eq!(first_ask.order.side, OrderSide::Sell);
1474 assert_eq!(first_ask.order.price, instrument.make_price(42501.0));
1475 assert_eq!(first_ask.order.size, instrument.make_qty(800.0, None));
1476
1477 let last = deltas.deltas.last().unwrap();
1479 assert_eq!(
1480 last.flags & RecordFlag::F_LAST as u8,
1481 RecordFlag::F_LAST as u8
1482 );
1483 }
1484
1485 #[rstest]
1486 fn test_parse_book_delta() {
1487 let instrument = test_perpetual_instrument();
1488 let json = load_test_json("ws_book_delta.json");
1489 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1490 let msg: DeribitBookMsg =
1491 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1492
1493 let deltas = parse_book_delta(&msg, &instrument, UnixNanos::default()).unwrap();
1494
1495 assert_eq!(deltas.instrument_id, instrument.id());
1496 assert_eq!(deltas.deltas.len(), 4);
1498
1499 let bid_change = &deltas.deltas[0];
1501 assert_eq!(bid_change.action, BookAction::Update);
1502 assert_eq!(bid_change.order.side, OrderSide::Buy);
1503 assert_eq!(bid_change.order.price, instrument.make_price(42500.0));
1504 assert_eq!(bid_change.order.size, instrument.make_qty(950.0, None));
1505
1506 let bid_new = &deltas.deltas[1];
1508 assert_eq!(bid_new.action, BookAction::Add);
1509 assert_eq!(bid_new.order.side, OrderSide::Buy);
1510 assert_eq!(bid_new.order.price, instrument.make_price(42498.5));
1511 assert_eq!(bid_new.order.size, instrument.make_qty(300.0, None));
1512
1513 let ask_delete = &deltas.deltas[2];
1515 assert_eq!(ask_delete.action, BookAction::Delete);
1516 assert_eq!(ask_delete.order.side, OrderSide::Sell);
1517 assert_eq!(ask_delete.order.price, instrument.make_price(42501.0));
1518 assert_eq!(ask_delete.order.size, instrument.make_qty(0.0, None));
1519
1520 let ask_change = &deltas.deltas[3];
1522 assert_eq!(ask_change.action, BookAction::Update);
1523 assert_eq!(ask_change.order.side, OrderSide::Sell);
1524 assert_eq!(ask_change.order.price, instrument.make_price(42501.5));
1525 assert_eq!(ask_change.order.size, instrument.make_qty(700.0, None));
1526
1527 let last = deltas.deltas.last().unwrap();
1529 assert_eq!(
1530 last.flags & RecordFlag::F_LAST as u8,
1531 RecordFlag::F_LAST as u8
1532 );
1533 }
1534
1535 #[rstest]
1536 fn test_parse_ticker_to_quote() {
1537 let instrument = test_perpetual_instrument();
1538 let json = load_test_json("ws_ticker.json");
1539 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1540 let msg: DeribitTickerMsg =
1541 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1542
1543 assert_eq!(msg.instrument_name.as_str(), "BTC-PERPETUAL");
1545 assert_eq!(msg.timestamp, 1_765_541_474_086);
1546 assert_eq!(msg.best_bid_price, Some(dec!(92283.5)));
1547 assert_eq!(msg.best_ask_price, Some(dec!(92284.0)));
1548 assert_eq!(msg.best_bid_amount, Some(dec!(117660.0)));
1549 assert_eq!(msg.best_ask_amount, Some(dec!(186520.0)));
1550 assert_eq!(msg.mark_price, dec!(92281.78));
1551 assert_eq!(msg.index_price, dec!(92263.55));
1552 assert_eq!(msg.open_interest, dec!(1132329370.0));
1553
1554 let quote = parse_ticker_to_quote(&msg, &instrument, UnixNanos::default()).unwrap();
1555
1556 assert_eq!(quote.instrument_id, instrument.id());
1557 assert_eq!(quote.bid_price, instrument.make_price(92283.5));
1558 assert_eq!(quote.ask_price, instrument.make_price(92284.0));
1559 assert_eq!(quote.bid_size, instrument.make_qty(117660.0, None));
1560 assert_eq!(quote.ask_size, instrument.make_qty(186520.0, None));
1561 assert_eq!(quote.ts_event, UnixNanos::new(1_765_541_474_086_000_000));
1562 }
1563
1564 #[rstest]
1565 fn test_parse_quote_msg() {
1566 let instrument = test_perpetual_instrument();
1567 let json = load_test_json("ws_quote.json");
1568 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1569 let msg: DeribitQuoteMsg =
1570 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1571
1572 assert_eq!(msg.instrument_name.as_str(), "BTC-PERPETUAL");
1574 assert_eq!(msg.timestamp, 1_765_541_767_174);
1575 assert_eq!(msg.best_bid_price, dec!(92288.0));
1576 assert_eq!(msg.best_ask_price, dec!(92288.5));
1577 assert_eq!(msg.best_bid_amount, dec!(133440.0));
1578 assert_eq!(msg.best_ask_amount, dec!(99470.0));
1579
1580 let quote = parse_quote_msg(&msg, &instrument, UnixNanos::default()).unwrap();
1581
1582 assert_eq!(quote.instrument_id, instrument.id());
1583 assert_eq!(quote.bid_price, instrument.make_price(92288.0));
1584 assert_eq!(quote.ask_price, instrument.make_price(92288.5));
1585 assert_eq!(quote.bid_size, instrument.make_qty(133440.0, None));
1586 assert_eq!(quote.ask_size, instrument.make_qty(99470.0, None));
1587 assert_eq!(quote.ts_event, UnixNanos::new(1_765_541_767_174_000_000));
1588 }
1589
1590 #[rstest]
1591 fn test_parse_book_msg_snapshot() {
1592 let instrument = test_perpetual_instrument();
1593 let json = load_test_json("ws_book_snapshot.json");
1594 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1595 let msg: DeribitBookMsg =
1596 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1597
1598 assert_eq!(
1600 msg.bids[0].len(),
1601 3,
1602 "Snapshot bids should have 3 elements: [action, price, amount]"
1603 );
1604 assert_eq!(
1605 msg.bids[0][0].as_str(),
1606 Some("new"),
1607 "First element should be 'new' action for snapshot"
1608 );
1609 assert_eq!(
1610 msg.asks[0].len(),
1611 3,
1612 "Snapshot asks should have 3 elements: [action, price, amount]"
1613 );
1614 assert_eq!(
1615 msg.asks[0][0].as_str(),
1616 Some("new"),
1617 "First element should be 'new' action for snapshot"
1618 );
1619
1620 let deltas = parse_book_msg(&msg, &instrument, UnixNanos::default()).unwrap();
1621
1622 assert_eq!(deltas.instrument_id, instrument.id());
1623 assert_eq!(deltas.deltas.len(), 11);
1625
1626 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
1628
1629 let first_bid = &deltas.deltas[1];
1631 assert_eq!(first_bid.action, BookAction::Add);
1632 assert_eq!(first_bid.order.side, OrderSide::Buy);
1633 assert_eq!(first_bid.order.price, instrument.make_price(42500.0));
1634 assert_eq!(first_bid.order.size, instrument.make_qty(1000.0, None));
1635
1636 let first_ask = &deltas.deltas[6];
1638 assert_eq!(first_ask.action, BookAction::Add);
1639 assert_eq!(first_ask.order.side, OrderSide::Sell);
1640 assert_eq!(first_ask.order.price, instrument.make_price(42501.0));
1641 assert_eq!(first_ask.order.size, instrument.make_qty(800.0, None));
1642 }
1643
1644 #[rstest]
1645 fn test_parse_book_msg_delta() {
1646 let instrument = test_perpetual_instrument();
1647 let json = load_test_json("ws_book_delta.json");
1648 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1649 let msg: DeribitBookMsg =
1650 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1651
1652 assert_eq!(
1654 msg.bids[0].len(),
1655 3,
1656 "Delta bids should have 3 elements: [action, price, amount]"
1657 );
1658 assert_eq!(
1659 msg.bids[0][0].as_str(),
1660 Some("change"),
1661 "First bid should be 'change' action"
1662 );
1663 assert_eq!(
1664 msg.bids[1][0].as_str(),
1665 Some("new"),
1666 "Second bid should be 'new' action"
1667 );
1668 assert_eq!(
1669 msg.asks[0].len(),
1670 3,
1671 "Delta asks should have 3 elements: [action, price, amount]"
1672 );
1673 assert_eq!(
1674 msg.asks[0][0].as_str(),
1675 Some("delete"),
1676 "First ask should be 'delete' action"
1677 );
1678
1679 let deltas = parse_book_msg(&msg, &instrument, UnixNanos::default()).unwrap();
1680
1681 assert_eq!(deltas.instrument_id, instrument.id());
1682 assert_eq!(deltas.deltas.len(), 4);
1684
1685 assert_ne!(deltas.deltas[0].action, BookAction::Clear);
1687
1688 let bid_change = &deltas.deltas[0];
1690 assert_eq!(bid_change.action, BookAction::Update);
1691 assert_eq!(bid_change.order.side, OrderSide::Buy);
1692 assert_eq!(bid_change.order.price, instrument.make_price(42500.0));
1693 assert_eq!(bid_change.order.size, instrument.make_qty(950.0, None));
1694
1695 let bid_new = &deltas.deltas[1];
1697 assert_eq!(bid_new.action, BookAction::Add);
1698 assert_eq!(bid_new.order.side, OrderSide::Buy);
1699 assert_eq!(bid_new.order.price, instrument.make_price(42498.5));
1700 assert_eq!(bid_new.order.size, instrument.make_qty(300.0, None));
1701
1702 let ask_delete = &deltas.deltas[2];
1704 assert_eq!(ask_delete.action, BookAction::Delete);
1705 assert_eq!(ask_delete.order.side, OrderSide::Sell);
1706 assert_eq!(ask_delete.order.price, instrument.make_price(42501.0));
1707
1708 let ask_change = &deltas.deltas[3];
1710 assert_eq!(ask_change.action, BookAction::Update);
1711 assert_eq!(ask_change.order.side, OrderSide::Sell);
1712 assert_eq!(ask_change.order.price, instrument.make_price(42501.5));
1713 assert_eq!(ask_change.order.size, instrument.make_qty(700.0, None));
1714 }
1715
1716 #[rstest]
1717 fn test_parse_book_grouped_snapshot() {
1718 let instrument = test_perpetual_instrument();
1721 let json = load_test_json("ws_book_grouped_snapshot.json");
1722 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1723 let msg: DeribitBookMsg =
1724 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1725
1726 assert_eq!(
1728 msg.bids[0].len(),
1729 2,
1730 "Grouped bids should have 2 elements: [price, amount]"
1731 );
1732 assert_eq!(
1733 msg.asks[0].len(),
1734 2,
1735 "Grouped asks should have 2 elements: [price, amount]"
1736 );
1737
1738 assert_eq!(
1740 msg.msg_type,
1741 DeribitBookMsgType::Snapshot,
1742 "Grouped channel should default to Snapshot type"
1743 );
1744
1745 let deltas = parse_book_snapshot(&msg, &instrument, UnixNanos::default()).unwrap();
1746
1747 assert_eq!(deltas.instrument_id, instrument.id());
1748 assert_eq!(deltas.deltas.len(), 21);
1750
1751 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
1753
1754 let first_bid = &deltas.deltas[1];
1756 assert_eq!(first_bid.action, BookAction::Add);
1757 assert_eq!(first_bid.order.side, OrderSide::Buy);
1758 assert_eq!(first_bid.order.price, instrument.make_price(89532.5));
1759 assert_eq!(first_bid.order.size, instrument.make_qty(254900.0, None));
1760
1761 let first_ask = &deltas.deltas[11];
1763 assert_eq!(first_ask.action, BookAction::Add);
1764 assert_eq!(first_ask.order.side, OrderSide::Sell);
1765 assert_eq!(first_ask.order.price, instrument.make_price(89533.0));
1766 assert_eq!(first_ask.order.size, instrument.make_qty(91570.0, None));
1767
1768 let last = deltas.deltas.last().unwrap();
1770 assert_eq!(
1771 last.flags & RecordFlag::F_LAST as u8,
1772 RecordFlag::F_LAST as u8
1773 );
1774 }
1775
1776 #[rstest]
1777 fn test_parse_ticker_to_mark_price() {
1778 let instrument = test_perpetual_instrument();
1779 let json = load_test_json("ws_ticker.json");
1780 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1781 let msg: DeribitTickerMsg =
1782 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1783
1784 let mark_price =
1785 parse_ticker_to_mark_price(&msg, &instrument, UnixNanos::default()).unwrap();
1786
1787 assert_eq!(mark_price.instrument_id, instrument.id());
1788 assert_eq!(mark_price.value, instrument.make_price(92281.78));
1789 assert_eq!(
1790 mark_price.ts_event,
1791 UnixNanos::new(1_765_541_474_086_000_000)
1792 );
1793 }
1794
1795 #[rstest]
1796 fn test_parse_ticker_to_index_price() {
1797 let instrument = test_perpetual_instrument();
1798 let json = load_test_json("ws_ticker.json");
1799 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1800 let msg: DeribitTickerMsg =
1801 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1802
1803 let index_price =
1804 parse_ticker_to_index_price(&msg, &instrument, UnixNanos::default()).unwrap();
1805
1806 assert_eq!(index_price.instrument_id, instrument.id());
1807 assert_eq!(index_price.value, instrument.make_price(92263.55));
1808 assert_eq!(
1809 index_price.ts_event,
1810 UnixNanos::new(1_765_541_474_086_000_000)
1811 );
1812 }
1813
1814 #[rstest]
1815 fn test_parse_ticker_to_funding_rate() {
1816 let instrument = test_perpetual_instrument();
1817 let json = load_test_json("ws_ticker.json");
1818 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1819 let msg: DeribitTickerMsg =
1820 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1821
1822 assert!(msg.current_funding.is_some());
1824
1825 let funding_rate =
1826 parse_ticker_to_funding_rate(&msg, &instrument, UnixNanos::default()).unwrap();
1827
1828 assert_eq!(funding_rate.instrument_id, instrument.id());
1829 assert_eq!(
1831 funding_rate.ts_event,
1832 UnixNanos::new(1_765_541_474_086_000_000)
1833 );
1834 assert!(funding_rate.interval.is_none());
1835 assert!(funding_rate.next_funding_ns.is_none()); }
1837
1838 #[rstest]
1839 fn test_resolution_to_bar_type_1_minute() {
1840 let instrument_id = InstrumentId::from("BTC-PERPETUAL.DERIBIT");
1841 let bar_type = resolution_to_bar_type(instrument_id, "1").unwrap();
1842
1843 assert_eq!(bar_type.instrument_id(), instrument_id);
1844 assert_eq!(bar_type.spec().step.get(), 1);
1845 assert_eq!(bar_type.spec().aggregation, BarAggregation::Minute);
1846 assert_eq!(bar_type.spec().price_type, PriceType::Last);
1847 assert_eq!(bar_type.aggregation_source(), AggregationSource::External);
1848 }
1849
1850 #[rstest]
1851 fn test_resolution_to_bar_type_60_minute() {
1852 let instrument_id = InstrumentId::from("ETH-PERPETUAL.DERIBIT");
1853 let bar_type = resolution_to_bar_type(instrument_id, "60").unwrap();
1854
1855 assert_eq!(bar_type.instrument_id(), instrument_id);
1856 assert_eq!(bar_type.spec().step.get(), 1);
1857 assert_eq!(bar_type.spec().aggregation, BarAggregation::Hour);
1858 }
1859
1860 #[rstest]
1861 fn test_resolution_to_bar_type_daily() {
1862 let instrument_id = InstrumentId::from("BTC-PERPETUAL.DERIBIT");
1863 let bar_type = resolution_to_bar_type(instrument_id, "1D").unwrap();
1864
1865 assert_eq!(bar_type.instrument_id(), instrument_id);
1866 assert_eq!(bar_type.spec().step.get(), 1);
1867 assert_eq!(bar_type.spec().aggregation, BarAggregation::Day);
1868 }
1869
1870 #[rstest]
1871 fn test_resolution_to_bar_type_invalid() {
1872 let instrument_id = InstrumentId::from("BTC-PERPETUAL.DERIBIT");
1873 let result = resolution_to_bar_type(instrument_id, "invalid");
1874
1875 assert!(result.is_err());
1876 assert!(
1877 result
1878 .unwrap_err()
1879 .to_string()
1880 .contains("Unsupported Deribit resolution")
1881 );
1882 }
1883
1884 #[rstest]
1885 fn test_parse_chart_msg_uses_cost() {
1886 let instrument = test_perpetual_instrument();
1887 assert!(
1888 instrument.is_inverse(),
1889 "test fixture is expected to be an inverse perp"
1890 );
1891
1892 let json = load_test_json("ws_chart.json");
1893 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1894 let chart_msg: DeribitChartMsg =
1895 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1896
1897 assert_eq!(chart_msg.tick, 1_767_200_040_000);
1899 assert_eq!(chart_msg.open, 87490.0);
1900 assert_eq!(chart_msg.high, 87500.0);
1901 assert_eq!(chart_msg.low, 87465.0);
1902 assert_eq!(chart_msg.close, 87474.0);
1903 assert_eq!(chart_msg.volume, 0.95978896);
1904 assert_eq!(chart_msg.cost, 83970.0);
1905
1906 let bar_type = resolution_to_bar_type(instrument.id(), "1").unwrap();
1907
1908 let bar = parse_chart_msg(
1910 &chart_msg,
1911 bar_type,
1912 instrument.price_precision(),
1913 instrument.size_precision(),
1914 true, true,
1916 UnixNanos::default(),
1917 )
1918 .unwrap();
1919
1920 assert_eq!(bar.bar_type, bar_type);
1921 assert_eq!(bar.open, instrument.make_price(87490.0));
1922 assert_eq!(bar.high, instrument.make_price(87500.0));
1923 assert_eq!(bar.low, instrument.make_price(87465.0));
1924 assert_eq!(bar.close, instrument.make_price(87474.0));
1925 assert_eq!(bar.volume, instrument.make_qty(83970.0, None));
1926
1927 assert_eq!(bar.ts_event, UnixNanos::new(1_767_200_100_000_000_000));
1929 }
1930
1931 #[rstest]
1932 fn test_parse_order_buy_response() {
1933 let instrument = test_perpetual_instrument();
1934 let json = load_test_json("ws_order_buy_response.json");
1935 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1936
1937 let order_msg: DeribitOrderMsg =
1939 serde_json::from_value(response["result"]["order"].clone()).unwrap();
1940
1941 assert_eq!(order_msg.order_id, "USDC-104819327443");
1943 assert_eq!(
1944 order_msg.label,
1945 Some("O-19700101-000000-001-001-1".to_string())
1946 );
1947 assert_eq!(order_msg.direction, "buy");
1948 assert_eq!(order_msg.order_state, "open");
1949 assert_eq!(order_msg.order_type, "limit");
1950 assert_eq!(order_msg.price, Some(dec!(2973.55)));
1951 assert_eq!(order_msg.amount, dec!(0.001));
1952 assert_eq!(order_msg.filled_amount, rust_decimal::Decimal::ZERO);
1953 assert!(order_msg.post_only);
1954 assert!(!order_msg.reduce_only);
1955
1956 let account_id = AccountId::new("DERIBIT-001");
1958 let trader_id = TraderId::new("TRADER-001");
1959 let strategy_id = StrategyId::new("PMM-001");
1960
1961 let accepted = parse_order_accepted(
1962 &order_msg,
1963 &instrument,
1964 account_id,
1965 trader_id,
1966 strategy_id,
1967 UnixNanos::default(),
1968 );
1969
1970 assert_eq!(
1971 accepted.client_order_id.to_string(),
1972 "O-19700101-000000-001-001-1"
1973 );
1974 assert_eq!(accepted.venue_order_id.to_string(), "USDC-104819327443");
1975 assert_eq!(accepted.trader_id, trader_id);
1976 assert_eq!(accepted.strategy_id, strategy_id);
1977 assert_eq!(accepted.account_id, account_id);
1978 }
1979
1980 #[rstest]
1981 fn test_parse_order_sell_response() {
1982 let instrument = test_perpetual_instrument();
1983 let json = load_test_json("ws_order_sell_response.json");
1984 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1985
1986 let order_msg: DeribitOrderMsg =
1987 serde_json::from_value(response["result"]["order"].clone()).unwrap();
1988
1989 assert_eq!(order_msg.order_id, "USDC-104819327458");
1991 assert_eq!(
1992 order_msg.label,
1993 Some("O-19700101-000000-001-001-2".to_string())
1994 );
1995 assert_eq!(order_msg.direction, "sell");
1996 assert_eq!(order_msg.order_state, "open");
1997 assert_eq!(order_msg.price, Some(dec!(3286.7)));
1998 assert_eq!(order_msg.amount, dec!(0.001));
1999
2000 let account_id = AccountId::new("DERIBIT-001");
2002 let trader_id = TraderId::new("TRADER-001");
2003 let strategy_id = StrategyId::new("PMM-001");
2004
2005 let accepted = parse_order_accepted(
2006 &order_msg,
2007 &instrument,
2008 account_id,
2009 trader_id,
2010 strategy_id,
2011 UnixNanos::default(),
2012 );
2013
2014 assert_eq!(
2015 accepted.client_order_id.to_string(),
2016 "O-19700101-000000-001-001-2"
2017 );
2018 assert_eq!(accepted.venue_order_id.to_string(), "USDC-104819327458");
2019 assert_eq!(accepted.trader_id, trader_id);
2020 assert_eq!(accepted.strategy_id, strategy_id);
2021 assert_eq!(accepted.account_id, account_id);
2022 }
2023
2024 #[rstest]
2025 fn test_parse_order_edit_response() {
2026 let instrument = test_perpetual_instrument();
2027 let json = load_test_json("ws_order_edit_response.json");
2028 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
2029
2030 let order_msg: DeribitOrderMsg =
2031 serde_json::from_value(response["result"]["order"].clone()).unwrap();
2032
2033 assert_eq!(order_msg.order_id, "USDC-104819327443");
2035 assert_eq!(
2036 order_msg.label,
2037 Some("O-19700101-000000-001-001-1".to_string())
2038 );
2039 assert_eq!(order_msg.direction, "buy");
2040 assert_eq!(order_msg.order_state, "open");
2041 assert_eq!(order_msg.price, Some(dec!(3067.2))); let account_id = AccountId::new("DERIBIT-001");
2045 let trader_id = TraderId::new("TRADER-001");
2046 let strategy_id = StrategyId::new("PMM-001");
2047
2048 let updated = parse_order_updated(
2049 &order_msg,
2050 &instrument,
2051 account_id,
2052 trader_id,
2053 strategy_id,
2054 UnixNanos::default(),
2055 );
2056
2057 assert_eq!(
2058 updated.client_order_id.to_string(),
2059 "O-19700101-000000-001-001-1"
2060 );
2061 assert_eq!(
2062 updated.venue_order_id.unwrap().to_string(),
2063 "USDC-104819327443"
2064 );
2065 assert_eq!(updated.quantity.as_f64(), 0.0);
2067 }
2068
2069 #[rstest]
2070 fn test_parse_order_cancel_response() {
2071 let instrument = test_perpetual_instrument();
2072 let json = load_test_json("ws_order_cancel_response.json");
2073 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
2074
2075 let order_msg: DeribitOrderMsg =
2077 serde_json::from_value(response["result"].clone()).unwrap();
2078
2079 assert_eq!(order_msg.order_id, "USDC-104819327443");
2081 assert_eq!(
2082 order_msg.label,
2083 Some("O-19700101-000000-001-001-1".to_string())
2084 );
2085 assert_eq!(order_msg.order_state, "cancelled");
2086 assert_eq!(order_msg.cancel_reason, Some("user_request".to_string()));
2087
2088 let account_id = AccountId::new("DERIBIT-001");
2090 let trader_id = TraderId::new("TRADER-001");
2091 let strategy_id = StrategyId::new("PMM-001");
2092
2093 let canceled = parse_order_canceled(
2094 &order_msg,
2095 &instrument,
2096 account_id,
2097 trader_id,
2098 strategy_id,
2099 UnixNanos::default(),
2100 );
2101
2102 assert_eq!(
2103 canceled.client_order_id.to_string(),
2104 "O-19700101-000000-001-001-1"
2105 );
2106 assert_eq!(
2107 canceled.venue_order_id.unwrap().to_string(),
2108 "USDC-104819327443"
2109 );
2110 assert_eq!(canceled.trader_id, trader_id);
2111 assert_eq!(canceled.strategy_id, strategy_id);
2112 }
2113
2114 #[rstest]
2115 fn test_parse_order_stop_market_response() {
2116 let instrument = test_perpetual_instrument();
2121 let json = load_test_json("ws_order_stop_market_response.json");
2122 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
2123
2124 let order_msg: DeribitOrderMsg =
2125 serde_json::from_value(response["result"]["order"].clone()).unwrap();
2126
2127 assert_eq!(order_msg.order_id, "USDC-104819327499");
2128 assert_eq!(order_msg.order_type, "stop_market");
2129 assert_eq!(order_msg.order_state, "untriggered");
2130 assert_eq!(order_msg.price, None);
2131 assert_eq!(order_msg.trigger_price, Some(dec!(2228.0)));
2132 assert_eq!(order_msg.trigger.as_deref(), Some("mark_price"));
2133 assert!(order_msg.reduce_only);
2134
2135 let account_id = AccountId::new("DERIBIT-001");
2136 let report =
2137 parse_user_order_msg(&order_msg, &instrument, account_id, UnixNanos::default())
2138 .unwrap();
2139
2140 assert_eq!(report.order_type, OrderType::StopMarket);
2141 assert_eq!(report.order_status, OrderStatus::Accepted);
2142 assert!(report.price.is_none());
2143 assert!(report.trigger_price.is_some());
2144 assert!(report.reduce_only);
2145 }
2146
2147 #[rstest]
2148 fn test_parse_order_stop_market_response_missing_filled_amount() {
2149 let instrument = test_perpetual_instrument();
2154 let json = load_test_json("ws_order_stop_market_no_filled_amount.json");
2155 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
2156
2157 let order_msg: DeribitOrderMsg =
2158 serde_json::from_value(response["result"]["order"].clone()).unwrap();
2159
2160 assert_eq!(order_msg.order_id, "USDC-SLMB-19641");
2161 assert_eq!(order_msg.order_type, "stop_market");
2162 assert_eq!(order_msg.order_state, "untriggered");
2163 assert_eq!(order_msg.filled_amount, rust_decimal::Decimal::ZERO);
2164 assert_eq!(order_msg.average_price, None);
2165
2166 let account_id = AccountId::new("DERIBIT-001");
2167 let report =
2168 parse_user_order_msg(&order_msg, &instrument, account_id, UnixNanos::default())
2169 .unwrap();
2170
2171 assert_eq!(report.order_type, OrderType::StopMarket);
2172 assert_eq!(report.order_status, OrderStatus::Accepted);
2173 assert_eq!(report.filled_qty.as_f64(), 0.0);
2174 }
2175
2176 #[rstest]
2177 fn test_parse_user_order_msg_to_status_report() {
2178 let instrument = test_perpetual_instrument();
2179 let json = load_test_json("ws_order_buy_response.json");
2180 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
2181
2182 let order_msg: DeribitOrderMsg =
2183 serde_json::from_value(response["result"]["order"].clone()).unwrap();
2184
2185 let account_id = AccountId::new("DERIBIT-001");
2186 let report =
2187 parse_user_order_msg(&order_msg, &instrument, account_id, UnixNanos::default())
2188 .unwrap();
2189
2190 assert_eq!(report.venue_order_id.to_string(), "USDC-104819327443");
2191 assert_eq!(
2192 report.client_order_id.unwrap().to_string(),
2193 "O-19700101-000000-001-001-1"
2194 );
2195 assert_eq!(report.order_side, OrderSide::Buy);
2196 assert_eq!(report.order_type, OrderType::Limit);
2197 assert_eq!(report.time_in_force, TimeInForce::Gtc);
2198 assert_eq!(report.order_status, OrderStatus::Accepted);
2199 assert_eq!(report.quantity.as_f64(), 0.0);
2201 assert_eq!(report.filled_qty.as_f64(), 0.0);
2202 assert!(report.post_only);
2203 assert!(!report.reduce_only);
2204 }
2205
2206 #[rstest]
2207 fn test_determine_order_event_type() {
2208 assert_eq!(
2210 determine_order_event_type("open", true, false),
2211 OrderEventType::Accepted
2212 );
2213
2214 assert_eq!(
2216 determine_order_event_type("open", false, true),
2217 OrderEventType::Updated
2218 );
2219
2220 assert_eq!(
2222 determine_order_event_type("cancelled", false, false),
2223 OrderEventType::Canceled
2224 );
2225
2226 assert_eq!(
2228 determine_order_event_type("expired", false, false),
2229 OrderEventType::Expired
2230 );
2231
2232 assert_eq!(
2234 determine_order_event_type("filled", false, false),
2235 OrderEventType::None
2236 );
2237 }
2238}