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