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 )
1113}
1114
1115#[must_use]
1120pub fn parse_order_expired(
1121 msg: &DeribitOrderMsg,
1122 instrument: &InstrumentAny,
1123 account_id: AccountId,
1124 trader_id: TraderId,
1125 strategy_id: StrategyId,
1126 ts_init: UnixNanos,
1127) -> OrderExpired {
1128 let client_order_id =
1129 extract_client_order_id(msg).unwrap_or_else(|| ClientOrderId::new(&msg.order_id));
1130 parse_order_expired_with_client_order_id(
1131 msg,
1132 instrument,
1133 account_id,
1134 trader_id,
1135 strategy_id,
1136 client_order_id,
1137 ts_init,
1138 )
1139}
1140
1141#[must_use]
1142pub(crate) fn parse_order_expired_with_client_order_id(
1143 msg: &DeribitOrderMsg,
1144 instrument: &InstrumentAny,
1145 account_id: AccountId,
1146 trader_id: TraderId,
1147 strategy_id: StrategyId,
1148 client_order_id: ClientOrderId,
1149 ts_init: UnixNanos,
1150) -> OrderExpired {
1151 let instrument_id = instrument.id();
1152 let venue_order_id = VenueOrderId::new(&msg.order_id);
1153 let ts_event = UnixNanos::new(msg.last_update_timestamp * NANOSECONDS_IN_MILLISECOND);
1154
1155 OrderExpired::new(
1156 trader_id,
1157 strategy_id,
1158 instrument_id,
1159 client_order_id,
1160 nautilus_core::UUID4::new(),
1161 ts_event,
1162 ts_init,
1163 false, Some(venue_order_id),
1165 Some(account_id),
1166 )
1167}
1168
1169#[must_use]
1173pub fn parse_order_updated(
1174 msg: &DeribitOrderMsg,
1175 instrument: &InstrumentAny,
1176 account_id: AccountId,
1177 trader_id: TraderId,
1178 strategy_id: StrategyId,
1179 ts_init: UnixNanos,
1180) -> OrderUpdated {
1181 let client_order_id =
1182 extract_client_order_id(msg).unwrap_or_else(|| ClientOrderId::new(&msg.order_id));
1183 parse_order_updated_with_client_order_id(
1184 msg,
1185 instrument,
1186 account_id,
1187 trader_id,
1188 strategy_id,
1189 client_order_id,
1190 ts_init,
1191 )
1192}
1193
1194#[must_use]
1195pub(crate) fn parse_order_updated_with_client_order_id(
1196 msg: &DeribitOrderMsg,
1197 instrument: &InstrumentAny,
1198 account_id: AccountId,
1199 trader_id: TraderId,
1200 strategy_id: StrategyId,
1201 client_order_id: ClientOrderId,
1202 ts_init: UnixNanos,
1203) -> OrderUpdated {
1204 let instrument_id = instrument.id();
1205 let price_precision = instrument.price_precision();
1206 let size_precision = instrument.size_precision();
1207
1208 let venue_order_id = VenueOrderId::new(&msg.order_id);
1209 let quantity = Quantity::from_decimal_dp(msg.amount, size_precision)
1210 .unwrap_or_else(|_| Quantity::zero(size_precision));
1211 let price = msg
1212 .price
1213 .and_then(|p| Price::from_decimal_dp(p, price_precision).ok());
1214 let trigger_price = msg
1215 .trigger_price
1216 .and_then(|p| Price::from_decimal_dp(p, price_precision).ok());
1217 let ts_event = UnixNanos::new(msg.last_update_timestamp * NANOSECONDS_IN_MILLISECOND);
1218
1219 OrderUpdated::new(
1220 trader_id,
1221 strategy_id,
1222 instrument_id,
1223 client_order_id,
1224 quantity,
1225 nautilus_core::UUID4::new(),
1226 ts_event,
1227 ts_init,
1228 false, Some(venue_order_id),
1230 Some(account_id),
1231 price,
1232 trigger_price,
1233 None, false, )
1236}
1237
1238#[must_use]
1251pub fn determine_order_event_type(
1252 order_state: &str,
1253 is_new_order: bool,
1254 was_amended: bool,
1255) -> OrderEventType {
1256 match order_state {
1257 "open" | "untriggered" => {
1258 if was_amended {
1259 OrderEventType::Updated
1260 } else if is_new_order {
1261 OrderEventType::Accepted
1262 } else {
1263 OrderEventType::None
1265 }
1266 }
1267 "cancelled" => OrderEventType::Canceled,
1268 "expired" => OrderEventType::Expired,
1269 "filled" => {
1270 OrderEventType::None
1272 }
1273 "rejected" => {
1274 OrderEventType::None
1276 }
1277 other => {
1278 log::warn!("Unknown Deribit order_state '{other}' in event routing, dropping");
1279 OrderEventType::None
1280 }
1281 }
1282}
1283
1284#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1286pub enum OrderEventType {
1287 Accepted,
1289 Canceled,
1291 Expired,
1293 Updated,
1295 None,
1297}
1298
1299#[cfg(test)]
1300mod tests {
1301 use rstest::rstest;
1302 use rust_decimal_macros::dec;
1303
1304 use super::*;
1305 use crate::{
1306 common::{parse::parse_deribit_instrument_any, testing::load_test_json},
1307 http::models::{DeribitInstrument, DeribitJsonRpcResponse},
1308 };
1309
1310 fn test_perpetual_instrument() -> InstrumentAny {
1312 let json = load_test_json("http_get_instruments.json");
1313 let response: DeribitJsonRpcResponse<Vec<DeribitInstrument>> =
1314 serde_json::from_str(&json).unwrap();
1315 let instrument = &response.result.unwrap()[0];
1316 parse_deribit_instrument_any(instrument, UnixNanos::default(), UnixNanos::default())
1317 .unwrap()
1318 .unwrap()
1319 }
1320
1321 #[rstest]
1322 fn test_parse_trade_msg_sell() {
1323 let instrument = test_perpetual_instrument();
1324 let json = load_test_json("ws_trades.json");
1325 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1326 let trades: Vec<DeribitTradeMsg> =
1327 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1328 let msg = &trades[0];
1329
1330 let tick = parse_trade_msg(msg, &instrument, UnixNanos::default()).unwrap();
1331
1332 assert_eq!(tick.instrument_id, instrument.id());
1333 assert_eq!(tick.price, instrument.make_price(92294.5));
1334 assert_eq!(tick.size, instrument.make_qty(10.0, None));
1335 assert_eq!(tick.aggressor_side, AggressorSide::Sell);
1336 assert_eq!(tick.trade_id.to_string(), "403691824");
1337 assert_eq!(tick.ts_event, UnixNanos::new(1_765_531_356_452_000_000));
1338 }
1339
1340 #[rstest]
1341 fn test_parse_trade_msg_buy() {
1342 let instrument = test_perpetual_instrument();
1343 let json = load_test_json("ws_trades.json");
1344 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1345 let trades: Vec<DeribitTradeMsg> =
1346 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1347 let msg = &trades[1];
1348
1349 let tick = parse_trade_msg(msg, &instrument, UnixNanos::default()).unwrap();
1350
1351 assert_eq!(tick.instrument_id, instrument.id());
1352 assert_eq!(tick.price, instrument.make_price(92288.5));
1353 assert_eq!(tick.size, instrument.make_qty(750.0, None));
1354 assert_eq!(tick.aggressor_side, AggressorSide::Sell);
1355 assert_eq!(tick.trade_id.to_string(), "403691825");
1356 }
1357
1358 fn make_trade_msg(
1359 instrument_name: &str,
1360 trade_id: &str,
1361 block_trade_id: Option<&str>,
1362 block_rfq_id: Option<i64>,
1363 combo_id: Option<&str>,
1364 ) -> DeribitTradeMsg {
1365 let raw = serde_json::json!({
1366 "trade_id": trade_id,
1367 "instrument_name": instrument_name,
1368 "price": 92294.5,
1369 "amount": 10.0,
1370 "direction": "buy",
1371 "timestamp": 1_765_531_356_452_u64,
1372 "trade_seq": 1,
1373 "tick_direction": 0,
1374 "index_price": 92276.75,
1375 "mark_price": 92287.11,
1376 "block_trade_id": block_trade_id,
1377 "block_rfq_id": block_rfq_id,
1378 "combo_id": combo_id,
1379 });
1380 serde_json::from_value(raw).unwrap()
1381 }
1382
1383 #[rstest]
1384 fn test_parse_trade_msg_tags_block_trade() {
1385 let instrument = test_perpetual_instrument();
1386 let msg = make_trade_msg("BTC-PERPETUAL", "244343055", Some("12345"), None, None);
1387 let tick = parse_trade_msg(&msg, &instrument, UnixNanos::default()).unwrap();
1388 assert_eq!(tick.trade_id.to_string(), "BLK-244343055");
1389 }
1390
1391 #[rstest]
1392 fn test_parse_trade_msg_tags_block_rfq() {
1393 let instrument = test_perpetual_instrument();
1394 let msg = make_trade_msg("BTC-PERPETUAL", "244343055", None, Some(99), None);
1395 let tick = parse_trade_msg(&msg, &instrument, UnixNanos::default()).unwrap();
1396 assert_eq!(tick.trade_id.to_string(), "RFQ-244343055");
1397 }
1398
1399 #[rstest]
1400 fn test_parse_trade_msg_tags_combo_leg() {
1401 let instrument = test_perpetual_instrument();
1405 let msg = make_trade_msg(
1406 "BTC-PERPETUAL",
1407 "244343055",
1408 None,
1409 None,
1410 Some("BTC-FS-25DEC26_PERP"),
1411 );
1412 let tick = parse_trade_msg(&msg, &instrument, UnixNanos::default()).unwrap();
1413 assert_eq!(tick.trade_id.to_string(), "COMBO-244343055");
1414 }
1415
1416 fn load_combo_option_instrument() -> InstrumentAny {
1417 let combo_json = load_test_json("http_get_instruments_option_combo.json");
1418 let combo_response: DeribitJsonRpcResponse<Vec<DeribitInstrument>> =
1419 serde_json::from_str(&combo_json).unwrap();
1420 let combo_raw = combo_response
1421 .result
1422 .unwrap()
1423 .into_iter()
1424 .find(|i| i.instrument_name.as_str() == "BTC-CS-19MAY26-70000_75000")
1425 .expect("fixture must contain BTC-CS-19MAY26-70000_75000");
1426 parse_deribit_instrument_any(&combo_raw, UnixNanos::default(), UnixNanos::default())
1427 .unwrap()
1428 .unwrap()
1429 }
1430
1431 fn load_combo_trade_msgs() -> Vec<DeribitTradeMsg> {
1432 let json = load_test_json("ws_trades_option_combo.json");
1433 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1434 serde_json::from_value(response["params"]["data"].clone()).unwrap()
1435 }
1436
1437 #[rstest]
1438 fn test_parse_trades_data_combo_emits_single_tick() {
1439 let combo_inst = load_combo_option_instrument();
1443 let mut cache: AHashMap<Ustr, InstrumentAny> = AHashMap::new();
1444 cache.insert(Ustr::from("BTC-CS-19MAY26-70000_75000"), combo_inst.clone());
1445
1446 let trades = load_combo_trade_msgs();
1447 let legs = trades[0].legs.as_ref().expect("combo trade must have legs");
1449 assert_eq!(legs.len(), 2);
1450
1451 let data = parse_trades_data(&trades, &cache, UnixNanos::default());
1452 assert_eq!(data.len(), 1, "should emit one tick for the combo only");
1453
1454 let Data::Trade(tick) = &data[0] else {
1455 panic!("expected Data::Trade");
1456 };
1457 assert_eq!(tick.instrument_id, combo_inst.id());
1458 assert_eq!(tick.price, Price::from("0.0639"));
1461 assert_eq!(tick.size, Quantity::from("0.1"));
1462 assert_eq!(tick.aggressor_side, AggressorSide::Sell);
1463 assert_eq!(tick.trade_id.to_string(), "244365193");
1464 }
1465
1466 #[rstest]
1467 fn test_parse_trades_data_combo_not_cached_emits_no_tick() {
1468 let cache: AHashMap<Ustr, InstrumentAny> = AHashMap::new();
1472 let trades = load_combo_trade_msgs();
1473 let data = parse_trades_data(&trades, &cache, UnixNanos::default());
1474 assert!(data.is_empty(), "uncached combo must not emit ticks");
1475 }
1476
1477 #[rstest]
1478 fn test_parse_trades_data_mixed_combo_and_per_leg() {
1479 let combo_inst = load_combo_option_instrument();
1482 let perp_inst = test_perpetual_instrument();
1483
1484 let mut cache: AHashMap<Ustr, InstrumentAny> = AHashMap::new();
1485 cache.insert(Ustr::from("BTC-CS-19MAY26-70000_75000"), combo_inst.clone());
1486 cache.insert(Ustr::from("BTC-PERPETUAL"), perp_inst.clone());
1487
1488 let mut trades = load_combo_trade_msgs();
1489 let perp_msgs: Vec<DeribitTradeMsg> = {
1490 let json = load_test_json("ws_trades.json");
1491 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1492 serde_json::from_value(response["params"]["data"].clone()).unwrap()
1493 };
1494 trades.extend(perp_msgs);
1495
1496 let data = parse_trades_data(&trades, &cache, UnixNanos::default());
1497 assert_eq!(data.len(), 3);
1499
1500 let mut combo_ticks = 0;
1501 let mut perp_ticks = 0;
1502
1503 for item in &data {
1504 let Data::Trade(tick) = item else {
1505 panic!("expected Data::Trade, was {item:?}");
1506 };
1507
1508 if tick.instrument_id == combo_inst.id() {
1509 combo_ticks += 1;
1510 assert_eq!(tick.trade_id.to_string(), "244365193");
1511 } else if tick.instrument_id == perp_inst.id() {
1512 perp_ticks += 1;
1513 } else {
1514 panic!("unexpected instrument_id: {}", tick.instrument_id);
1515 }
1516 }
1517 assert_eq!(combo_ticks, 1);
1518 assert_eq!(perp_ticks, 2);
1519 }
1520
1521 #[rstest]
1522 fn test_parse_book_snapshot() {
1523 let instrument = test_perpetual_instrument();
1524 let json = load_test_json("ws_book_snapshot.json");
1525 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1526 let msg: DeribitBookMsg =
1527 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1528
1529 let deltas = parse_book_snapshot(&msg, &instrument, UnixNanos::default()).unwrap();
1530
1531 assert_eq!(deltas.instrument_id, instrument.id());
1532 assert_eq!(deltas.deltas.len(), 11);
1534
1535 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
1537
1538 let first_bid = &deltas.deltas[1];
1540 assert_eq!(first_bid.action, BookAction::Add);
1541 assert_eq!(first_bid.order.side, OrderSide::Buy.into());
1542 assert_eq!(first_bid.order.price, instrument.make_price(42500.0));
1543 assert_eq!(first_bid.order.size, instrument.make_qty(1000.0, None));
1544
1545 let first_ask = &deltas.deltas[6];
1547 assert_eq!(first_ask.action, BookAction::Add);
1548 assert_eq!(first_ask.order.side, OrderSide::Sell.into());
1549 assert_eq!(first_ask.order.price, instrument.make_price(42501.0));
1550 assert_eq!(first_ask.order.size, instrument.make_qty(800.0, None));
1551
1552 let last = deltas.deltas.last().unwrap();
1554 assert_eq!(
1555 last.flags & RecordFlag::F_LAST as u8,
1556 RecordFlag::F_LAST as u8
1557 );
1558 }
1559
1560 #[rstest]
1561 fn test_parse_book_delta() {
1562 let instrument = test_perpetual_instrument();
1563 let json = load_test_json("ws_book_delta.json");
1564 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1565 let msg: DeribitBookMsg =
1566 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1567
1568 let deltas = parse_book_delta(&msg, &instrument, UnixNanos::default()).unwrap();
1569
1570 assert_eq!(deltas.instrument_id, instrument.id());
1571 assert_eq!(deltas.deltas.len(), 4);
1573
1574 let bid_change = &deltas.deltas[0];
1576 assert_eq!(bid_change.action, BookAction::Update);
1577 assert_eq!(bid_change.order.side, OrderSide::Buy.into());
1578 assert_eq!(bid_change.order.price, instrument.make_price(42500.0));
1579 assert_eq!(bid_change.order.size, instrument.make_qty(950.0, None));
1580
1581 let bid_new = &deltas.deltas[1];
1583 assert_eq!(bid_new.action, BookAction::Add);
1584 assert_eq!(bid_new.order.side, OrderSide::Buy.into());
1585 assert_eq!(bid_new.order.price, instrument.make_price(42498.5));
1586 assert_eq!(bid_new.order.size, instrument.make_qty(300.0, None));
1587
1588 let ask_delete = &deltas.deltas[2];
1590 assert_eq!(ask_delete.action, BookAction::Delete);
1591 assert_eq!(ask_delete.order.side, OrderSide::Sell.into());
1592 assert_eq!(ask_delete.order.price, instrument.make_price(42501.0));
1593 assert_eq!(ask_delete.order.size, instrument.make_qty(0.0, None));
1594
1595 let ask_change = &deltas.deltas[3];
1597 assert_eq!(ask_change.action, BookAction::Update);
1598 assert_eq!(ask_change.order.side, OrderSide::Sell.into());
1599 assert_eq!(ask_change.order.price, instrument.make_price(42501.5));
1600 assert_eq!(ask_change.order.size, instrument.make_qty(700.0, None));
1601
1602 let last = deltas.deltas.last().unwrap();
1604 assert_eq!(
1605 last.flags & RecordFlag::F_LAST as u8,
1606 RecordFlag::F_LAST as u8
1607 );
1608 }
1609
1610 #[rstest]
1611 fn test_parse_ticker_to_quote() {
1612 let instrument = test_perpetual_instrument();
1613 let json = load_test_json("ws_ticker.json");
1614 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1615 let msg: DeribitTickerMsg =
1616 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1617
1618 assert_eq!(msg.instrument_name.as_str(), "BTC-PERPETUAL");
1620 assert_eq!(msg.timestamp, 1_765_541_474_086);
1621 assert_eq!(msg.best_bid_price, Some(dec!(92283.5)));
1622 assert_eq!(msg.best_ask_price, Some(dec!(92284.0)));
1623 assert_eq!(msg.best_bid_amount, Some(dec!(117660.0)));
1624 assert_eq!(msg.best_ask_amount, Some(dec!(186520.0)));
1625 assert_eq!(msg.mark_price, dec!(92281.78));
1626 assert_eq!(msg.index_price, dec!(92263.55));
1627 assert_eq!(msg.open_interest, dec!(1132329370.0));
1628
1629 let quote = parse_ticker_to_quote(&msg, &instrument, UnixNanos::default()).unwrap();
1630
1631 assert_eq!(quote.instrument_id, instrument.id());
1632 assert_eq!(quote.bid_price, instrument.make_price(92283.5));
1633 assert_eq!(quote.ask_price, instrument.make_price(92284.0));
1634 assert_eq!(quote.bid_size, instrument.make_qty(117660.0, None));
1635 assert_eq!(quote.ask_size, instrument.make_qty(186520.0, None));
1636 assert_eq!(quote.ts_event, UnixNanos::new(1_765_541_474_086_000_000));
1637 }
1638
1639 #[rstest]
1640 fn test_parse_quote_msg() {
1641 let instrument = test_perpetual_instrument();
1642 let json = load_test_json("ws_quote.json");
1643 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1644 let msg: DeribitQuoteMsg =
1645 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1646
1647 assert_eq!(msg.instrument_name.as_str(), "BTC-PERPETUAL");
1649 assert_eq!(msg.timestamp, 1_765_541_767_174);
1650 assert_eq!(msg.best_bid_price, dec!(92288.0));
1651 assert_eq!(msg.best_ask_price, dec!(92288.5));
1652 assert_eq!(msg.best_bid_amount, dec!(133440.0));
1653 assert_eq!(msg.best_ask_amount, dec!(99470.0));
1654
1655 let quote = parse_quote_msg(&msg, &instrument, UnixNanos::default()).unwrap();
1656
1657 assert_eq!(quote.instrument_id, instrument.id());
1658 assert_eq!(quote.bid_price, instrument.make_price(92288.0));
1659 assert_eq!(quote.ask_price, instrument.make_price(92288.5));
1660 assert_eq!(quote.bid_size, instrument.make_qty(133440.0, None));
1661 assert_eq!(quote.ask_size, instrument.make_qty(99470.0, None));
1662 assert_eq!(quote.ts_event, UnixNanos::new(1_765_541_767_174_000_000));
1663 }
1664
1665 #[rstest]
1666 fn test_parse_book_msg_snapshot() {
1667 let instrument = test_perpetual_instrument();
1668 let json = load_test_json("ws_book_snapshot.json");
1669 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1670 let msg: DeribitBookMsg =
1671 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1672
1673 assert_eq!(
1675 msg.bids[0].len(),
1676 3,
1677 "Snapshot bids should have 3 elements: [action, price, amount]"
1678 );
1679 assert_eq!(
1680 msg.bids[0][0].as_str(),
1681 Some("new"),
1682 "First element should be 'new' action for snapshot"
1683 );
1684 assert_eq!(
1685 msg.asks[0].len(),
1686 3,
1687 "Snapshot asks should have 3 elements: [action, price, amount]"
1688 );
1689 assert_eq!(
1690 msg.asks[0][0].as_str(),
1691 Some("new"),
1692 "First element should be 'new' action for snapshot"
1693 );
1694
1695 let deltas = parse_book_msg(&msg, &instrument, UnixNanos::default()).unwrap();
1696
1697 assert_eq!(deltas.instrument_id, instrument.id());
1698 assert_eq!(deltas.deltas.len(), 11);
1700
1701 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
1703
1704 let first_bid = &deltas.deltas[1];
1706 assert_eq!(first_bid.action, BookAction::Add);
1707 assert_eq!(first_bid.order.side, OrderSide::Buy.into());
1708 assert_eq!(first_bid.order.price, instrument.make_price(42500.0));
1709 assert_eq!(first_bid.order.size, instrument.make_qty(1000.0, None));
1710
1711 let first_ask = &deltas.deltas[6];
1713 assert_eq!(first_ask.action, BookAction::Add);
1714 assert_eq!(first_ask.order.side, OrderSide::Sell.into());
1715 assert_eq!(first_ask.order.price, instrument.make_price(42501.0));
1716 assert_eq!(first_ask.order.size, instrument.make_qty(800.0, None));
1717 }
1718
1719 #[rstest]
1720 fn test_parse_book_msg_delta() {
1721 let instrument = test_perpetual_instrument();
1722 let json = load_test_json("ws_book_delta.json");
1723 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1724 let msg: DeribitBookMsg =
1725 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1726
1727 assert_eq!(
1729 msg.bids[0].len(),
1730 3,
1731 "Delta bids should have 3 elements: [action, price, amount]"
1732 );
1733 assert_eq!(
1734 msg.bids[0][0].as_str(),
1735 Some("change"),
1736 "First bid should be 'change' action"
1737 );
1738 assert_eq!(
1739 msg.bids[1][0].as_str(),
1740 Some("new"),
1741 "Second bid should be 'new' action"
1742 );
1743 assert_eq!(
1744 msg.asks[0].len(),
1745 3,
1746 "Delta asks should have 3 elements: [action, price, amount]"
1747 );
1748 assert_eq!(
1749 msg.asks[0][0].as_str(),
1750 Some("delete"),
1751 "First ask should be 'delete' action"
1752 );
1753
1754 let deltas = parse_book_msg(&msg, &instrument, UnixNanos::default()).unwrap();
1755
1756 assert_eq!(deltas.instrument_id, instrument.id());
1757 assert_eq!(deltas.deltas.len(), 4);
1759
1760 assert_ne!(deltas.deltas[0].action, BookAction::Clear);
1762
1763 let bid_change = &deltas.deltas[0];
1765 assert_eq!(bid_change.action, BookAction::Update);
1766 assert_eq!(bid_change.order.side, OrderSide::Buy.into());
1767 assert_eq!(bid_change.order.price, instrument.make_price(42500.0));
1768 assert_eq!(bid_change.order.size, instrument.make_qty(950.0, None));
1769
1770 let bid_new = &deltas.deltas[1];
1772 assert_eq!(bid_new.action, BookAction::Add);
1773 assert_eq!(bid_new.order.side, OrderSide::Buy.into());
1774 assert_eq!(bid_new.order.price, instrument.make_price(42498.5));
1775 assert_eq!(bid_new.order.size, instrument.make_qty(300.0, None));
1776
1777 let ask_delete = &deltas.deltas[2];
1779 assert_eq!(ask_delete.action, BookAction::Delete);
1780 assert_eq!(ask_delete.order.side, OrderSide::Sell.into());
1781 assert_eq!(ask_delete.order.price, instrument.make_price(42501.0));
1782
1783 let ask_change = &deltas.deltas[3];
1785 assert_eq!(ask_change.action, BookAction::Update);
1786 assert_eq!(ask_change.order.side, OrderSide::Sell.into());
1787 assert_eq!(ask_change.order.price, instrument.make_price(42501.5));
1788 assert_eq!(ask_change.order.size, instrument.make_qty(700.0, None));
1789 }
1790
1791 #[rstest]
1792 fn test_parse_book_grouped_snapshot() {
1793 let instrument = test_perpetual_instrument();
1796 let json = load_test_json("ws_book_grouped_snapshot.json");
1797 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1798 let msg: DeribitBookMsg =
1799 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1800
1801 assert_eq!(
1803 msg.bids[0].len(),
1804 2,
1805 "Grouped bids should have 2 elements: [price, amount]"
1806 );
1807 assert_eq!(
1808 msg.asks[0].len(),
1809 2,
1810 "Grouped asks should have 2 elements: [price, amount]"
1811 );
1812
1813 assert_eq!(
1815 msg.msg_type,
1816 DeribitBookMsgType::Snapshot,
1817 "Grouped channel should default to Snapshot type"
1818 );
1819
1820 let deltas = parse_book_snapshot(&msg, &instrument, UnixNanos::default()).unwrap();
1821
1822 assert_eq!(deltas.instrument_id, instrument.id());
1823 assert_eq!(deltas.deltas.len(), 21);
1825
1826 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
1828
1829 let first_bid = &deltas.deltas[1];
1831 assert_eq!(first_bid.action, BookAction::Add);
1832 assert_eq!(first_bid.order.side, OrderSide::Buy.into());
1833 assert_eq!(first_bid.order.price, instrument.make_price(89532.5));
1834 assert_eq!(first_bid.order.size, instrument.make_qty(254900.0, None));
1835
1836 let first_ask = &deltas.deltas[11];
1838 assert_eq!(first_ask.action, BookAction::Add);
1839 assert_eq!(first_ask.order.side, OrderSide::Sell.into());
1840 assert_eq!(first_ask.order.price, instrument.make_price(89533.0));
1841 assert_eq!(first_ask.order.size, instrument.make_qty(91570.0, None));
1842
1843 let last = deltas.deltas.last().unwrap();
1845 assert_eq!(
1846 last.flags & RecordFlag::F_LAST as u8,
1847 RecordFlag::F_LAST as u8
1848 );
1849 }
1850
1851 #[rstest]
1852 fn test_parse_ticker_to_mark_price() {
1853 let instrument = test_perpetual_instrument();
1854 let json = load_test_json("ws_ticker.json");
1855 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1856 let msg: DeribitTickerMsg =
1857 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1858
1859 let mark_price =
1860 parse_ticker_to_mark_price(&msg, &instrument, UnixNanos::default()).unwrap();
1861
1862 assert_eq!(mark_price.instrument_id, instrument.id());
1863 assert_eq!(mark_price.value, instrument.make_price(92281.78));
1864 assert_eq!(
1865 mark_price.ts_event,
1866 UnixNanos::new(1_765_541_474_086_000_000)
1867 );
1868 }
1869
1870 #[rstest]
1871 fn test_parse_ticker_to_index_price() {
1872 let instrument = test_perpetual_instrument();
1873 let json = load_test_json("ws_ticker.json");
1874 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1875 let msg: DeribitTickerMsg =
1876 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1877
1878 let index_price =
1879 parse_ticker_to_index_price(&msg, &instrument, UnixNanos::default()).unwrap();
1880
1881 assert_eq!(index_price.instrument_id, instrument.id());
1882 assert_eq!(index_price.value, instrument.make_price(92263.55));
1883 assert_eq!(
1884 index_price.ts_event,
1885 UnixNanos::new(1_765_541_474_086_000_000)
1886 );
1887 }
1888
1889 #[rstest]
1890 fn test_parse_ticker_to_funding_rate() {
1891 let instrument = test_perpetual_instrument();
1892 let json = load_test_json("ws_ticker.json");
1893 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1894 let msg: DeribitTickerMsg =
1895 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1896
1897 assert!(msg.current_funding.is_some());
1899
1900 let funding_rate =
1901 parse_ticker_to_funding_rate(&msg, &instrument, UnixNanos::default()).unwrap();
1902
1903 assert_eq!(funding_rate.instrument_id, instrument.id());
1904 assert_eq!(
1906 funding_rate.ts_event,
1907 UnixNanos::new(1_765_541_474_086_000_000)
1908 );
1909 assert!(funding_rate.interval.is_none());
1910 assert!(funding_rate.next_funding_ns.is_none()); }
1912
1913 #[rstest]
1914 fn test_resolution_to_bar_type_1_minute() {
1915 let instrument_id = InstrumentId::from("BTC-PERPETUAL.DERIBIT");
1916 let bar_type = resolution_to_bar_type(instrument_id, "1").unwrap();
1917
1918 assert_eq!(bar_type.instrument_id(), instrument_id);
1919 assert_eq!(bar_type.spec().step.get(), 1);
1920 assert_eq!(bar_type.spec().aggregation, BarAggregation::Minute);
1921 assert_eq!(bar_type.spec().price_type, PriceType::Last);
1922 assert_eq!(bar_type.aggregation_source(), AggregationSource::External);
1923 }
1924
1925 #[rstest]
1926 fn test_resolution_to_bar_type_60_minute() {
1927 let instrument_id = InstrumentId::from("ETH-PERPETUAL.DERIBIT");
1928 let bar_type = resolution_to_bar_type(instrument_id, "60").unwrap();
1929
1930 assert_eq!(bar_type.instrument_id(), instrument_id);
1931 assert_eq!(bar_type.spec().step.get(), 1);
1932 assert_eq!(bar_type.spec().aggregation, BarAggregation::Hour);
1933 }
1934
1935 #[rstest]
1936 fn test_resolution_to_bar_type_daily() {
1937 let instrument_id = InstrumentId::from("BTC-PERPETUAL.DERIBIT");
1938 let bar_type = resolution_to_bar_type(instrument_id, "1D").unwrap();
1939
1940 assert_eq!(bar_type.instrument_id(), instrument_id);
1941 assert_eq!(bar_type.spec().step.get(), 1);
1942 assert_eq!(bar_type.spec().aggregation, BarAggregation::Day);
1943 }
1944
1945 #[rstest]
1946 fn test_resolution_to_bar_type_invalid() {
1947 let instrument_id = InstrumentId::from("BTC-PERPETUAL.DERIBIT");
1948 let result = resolution_to_bar_type(instrument_id, "invalid");
1949
1950 assert!(result.is_err());
1951 assert!(
1952 result
1953 .unwrap_err()
1954 .to_string()
1955 .contains("Unsupported Deribit resolution")
1956 );
1957 }
1958
1959 #[rstest]
1960 fn test_parse_chart_msg_uses_cost() {
1961 let instrument = test_perpetual_instrument();
1962 assert!(
1963 instrument.is_inverse(),
1964 "test fixture is expected to be an inverse perp"
1965 );
1966
1967 let json = load_test_json("ws_chart.json");
1968 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
1969 let chart_msg: DeribitChartMsg =
1970 serde_json::from_value(response["params"]["data"].clone()).unwrap();
1971
1972 assert_eq!(chart_msg.tick, 1_767_200_040_000);
1974 assert_eq!(chart_msg.open, 87490.0);
1975 assert_eq!(chart_msg.high, 87500.0);
1976 assert_eq!(chart_msg.low, 87465.0);
1977 assert_eq!(chart_msg.close, 87474.0);
1978 assert_eq!(chart_msg.volume, 0.95978896);
1979 assert_eq!(chart_msg.cost, 83970.0);
1980
1981 let bar_type = resolution_to_bar_type(instrument.id(), "1").unwrap();
1982
1983 let bar = parse_chart_msg(
1985 &chart_msg,
1986 bar_type,
1987 instrument.price_precision(),
1988 instrument.size_precision(),
1989 true, true,
1991 UnixNanos::default(),
1992 )
1993 .unwrap();
1994
1995 assert_eq!(bar.bar_type, bar_type);
1996 assert_eq!(bar.open, instrument.make_price(87490.0));
1997 assert_eq!(bar.high, instrument.make_price(87500.0));
1998 assert_eq!(bar.low, instrument.make_price(87465.0));
1999 assert_eq!(bar.close, instrument.make_price(87474.0));
2000 assert_eq!(bar.volume, instrument.make_qty(83970.0, None));
2001
2002 assert_eq!(bar.ts_event, UnixNanos::new(1_767_200_100_000_000_000));
2004 }
2005
2006 #[rstest]
2007 fn test_parse_order_buy_response() {
2008 let instrument = test_perpetual_instrument();
2009 let json = load_test_json("ws_order_buy_response.json");
2010 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
2011
2012 let order_msg: DeribitOrderMsg =
2014 serde_json::from_value(response["result"]["order"].clone()).unwrap();
2015
2016 assert_eq!(order_msg.order_id, "USDC-104819327443");
2018 assert_eq!(
2019 order_msg.label,
2020 Some("O-19700101-000000-001-001-1".to_string())
2021 );
2022 assert_eq!(order_msg.direction, "buy");
2023 assert_eq!(order_msg.order_state, "open");
2024 assert_eq!(order_msg.order_type, "limit");
2025 assert_eq!(order_msg.price, Some(dec!(2973.55)));
2026 assert_eq!(order_msg.amount, dec!(0.001));
2027 assert_eq!(order_msg.filled_amount, rust_decimal::Decimal::ZERO);
2028 assert!(order_msg.post_only);
2029 assert!(!order_msg.reduce_only);
2030
2031 let account_id = AccountId::new("DERIBIT-001");
2033 let trader_id = TraderId::new("TRADER-001");
2034 let strategy_id = StrategyId::new("PMM-001");
2035
2036 let accepted = parse_order_accepted(
2037 &order_msg,
2038 &instrument,
2039 account_id,
2040 trader_id,
2041 strategy_id,
2042 UnixNanos::default(),
2043 );
2044
2045 assert_eq!(
2046 accepted.client_order_id.to_string(),
2047 "O-19700101-000000-001-001-1"
2048 );
2049 assert_eq!(accepted.venue_order_id.to_string(), "USDC-104819327443");
2050 assert_eq!(accepted.trader_id, trader_id);
2051 assert_eq!(accepted.strategy_id, strategy_id);
2052 assert_eq!(accepted.account_id, account_id);
2053 }
2054
2055 #[rstest]
2056 fn test_parse_order_sell_response() {
2057 let instrument = test_perpetual_instrument();
2058 let json = load_test_json("ws_order_sell_response.json");
2059 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
2060
2061 let order_msg: DeribitOrderMsg =
2062 serde_json::from_value(response["result"]["order"].clone()).unwrap();
2063
2064 assert_eq!(order_msg.order_id, "USDC-104819327458");
2066 assert_eq!(
2067 order_msg.label,
2068 Some("O-19700101-000000-001-001-2".to_string())
2069 );
2070 assert_eq!(order_msg.direction, "sell");
2071 assert_eq!(order_msg.order_state, "open");
2072 assert_eq!(order_msg.price, Some(dec!(3286.7)));
2073 assert_eq!(order_msg.amount, dec!(0.001));
2074
2075 let account_id = AccountId::new("DERIBIT-001");
2077 let trader_id = TraderId::new("TRADER-001");
2078 let strategy_id = StrategyId::new("PMM-001");
2079
2080 let accepted = parse_order_accepted(
2081 &order_msg,
2082 &instrument,
2083 account_id,
2084 trader_id,
2085 strategy_id,
2086 UnixNanos::default(),
2087 );
2088
2089 assert_eq!(
2090 accepted.client_order_id.to_string(),
2091 "O-19700101-000000-001-001-2"
2092 );
2093 assert_eq!(accepted.venue_order_id.to_string(), "USDC-104819327458");
2094 assert_eq!(accepted.trader_id, trader_id);
2095 assert_eq!(accepted.strategy_id, strategy_id);
2096 assert_eq!(accepted.account_id, account_id);
2097 }
2098
2099 #[rstest]
2100 fn test_parse_order_edit_response() {
2101 let instrument = test_perpetual_instrument();
2102 let json = load_test_json("ws_order_edit_response.json");
2103 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
2104
2105 let order_msg: DeribitOrderMsg =
2106 serde_json::from_value(response["result"]["order"].clone()).unwrap();
2107
2108 assert_eq!(order_msg.order_id, "USDC-104819327443");
2110 assert_eq!(
2111 order_msg.label,
2112 Some("O-19700101-000000-001-001-1".to_string())
2113 );
2114 assert_eq!(order_msg.direction, "buy");
2115 assert_eq!(order_msg.order_state, "open");
2116 assert!(order_msg.replaced);
2117 assert_eq!(order_msg.price, Some(dec!(3067.2))); let account_id = AccountId::new("DERIBIT-001");
2121 let trader_id = TraderId::new("TRADER-001");
2122 let strategy_id = StrategyId::new("PMM-001");
2123
2124 let updated = parse_order_updated(
2125 &order_msg,
2126 &instrument,
2127 account_id,
2128 trader_id,
2129 strategy_id,
2130 UnixNanos::default(),
2131 );
2132
2133 assert_eq!(
2134 updated.client_order_id.to_string(),
2135 "O-19700101-000000-001-001-1"
2136 );
2137 assert_eq!(
2138 updated.venue_order_id.unwrap().to_string(),
2139 "USDC-104819327443"
2140 );
2141 assert_eq!(updated.quantity.as_f64(), 0.0);
2143 }
2144
2145 #[rstest]
2146 fn test_parse_order_cancel_response() {
2147 let instrument = test_perpetual_instrument();
2148 let json = load_test_json("ws_order_cancel_response.json");
2149 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
2150
2151 let order_msg: DeribitOrderMsg =
2153 serde_json::from_value(response["result"].clone()).unwrap();
2154
2155 assert_eq!(order_msg.order_id, "USDC-104819327443");
2157 assert_eq!(
2158 order_msg.label,
2159 Some("O-19700101-000000-001-001-1".to_string())
2160 );
2161 assert_eq!(order_msg.order_state, "cancelled");
2162 assert_eq!(order_msg.cancel_reason, Some("user_request".to_string()));
2163
2164 let account_id = AccountId::new("DERIBIT-001");
2166 let trader_id = TraderId::new("TRADER-001");
2167 let strategy_id = StrategyId::new("PMM-001");
2168
2169 let canceled = parse_order_canceled(
2170 &order_msg,
2171 &instrument,
2172 account_id,
2173 trader_id,
2174 strategy_id,
2175 UnixNanos::default(),
2176 );
2177
2178 assert_eq!(
2179 canceled.client_order_id.to_string(),
2180 "O-19700101-000000-001-001-1"
2181 );
2182 assert_eq!(
2183 canceled.venue_order_id.unwrap().to_string(),
2184 "USDC-104819327443"
2185 );
2186 assert_eq!(canceled.trader_id, trader_id);
2187 assert_eq!(canceled.strategy_id, strategy_id);
2188 }
2189
2190 #[rstest]
2191 fn test_parse_order_stop_market_response() {
2192 let instrument = test_perpetual_instrument();
2197 let json = load_test_json("ws_order_stop_market_response.json");
2198 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
2199
2200 let order_msg: DeribitOrderMsg =
2201 serde_json::from_value(response["result"]["order"].clone()).unwrap();
2202
2203 assert_eq!(order_msg.order_id, "USDC-104819327499");
2204 assert_eq!(order_msg.order_type, "stop_market");
2205 assert_eq!(order_msg.order_state, "untriggered");
2206 assert_eq!(order_msg.price, None);
2207 assert_eq!(order_msg.trigger_price, Some(dec!(2228.0)));
2208 assert_eq!(order_msg.trigger.as_deref(), Some("mark_price"));
2209 assert!(order_msg.reduce_only);
2210
2211 let account_id = AccountId::new("DERIBIT-001");
2212 let report =
2213 parse_user_order_msg(&order_msg, &instrument, account_id, UnixNanos::default())
2214 .unwrap();
2215
2216 assert_eq!(report.order_type, OrderType::StopMarket);
2217 assert_eq!(report.order_status, OrderStatus::Accepted);
2218 assert!(report.price.is_none());
2219 assert!(report.trigger_price.is_some());
2220 assert!(report.reduce_only);
2221 }
2222
2223 #[rstest]
2224 fn test_parse_order_stop_market_response_missing_filled_amount() {
2225 let instrument = test_perpetual_instrument();
2230 let json = load_test_json("ws_order_stop_market_no_filled_amount.json");
2231 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
2232
2233 let order_msg: DeribitOrderMsg =
2234 serde_json::from_value(response["result"]["order"].clone()).unwrap();
2235
2236 assert_eq!(order_msg.order_id, "USDC-SLMB-19641");
2237 assert_eq!(order_msg.order_type, "stop_market");
2238 assert_eq!(order_msg.order_state, "untriggered");
2239 assert_eq!(order_msg.filled_amount, rust_decimal::Decimal::ZERO);
2240 assert_eq!(order_msg.average_price, None);
2241
2242 let account_id = AccountId::new("DERIBIT-001");
2243 let report =
2244 parse_user_order_msg(&order_msg, &instrument, account_id, UnixNanos::default())
2245 .unwrap();
2246
2247 assert_eq!(report.order_type, OrderType::StopMarket);
2248 assert_eq!(report.order_status, OrderStatus::Accepted);
2249 assert_eq!(report.filled_qty.as_f64(), 0.0);
2250 }
2251
2252 #[rstest]
2253 fn test_parse_user_order_msg_to_status_report() {
2254 let instrument = test_perpetual_instrument();
2255 let json = load_test_json("ws_order_buy_response.json");
2256 let response: serde_json::Value = serde_json::from_str(&json).unwrap();
2257
2258 let order_msg: DeribitOrderMsg =
2259 serde_json::from_value(response["result"]["order"].clone()).unwrap();
2260
2261 let account_id = AccountId::new("DERIBIT-001");
2262 let report =
2263 parse_user_order_msg(&order_msg, &instrument, account_id, UnixNanos::default())
2264 .unwrap();
2265
2266 assert_eq!(report.venue_order_id.to_string(), "USDC-104819327443");
2267 assert_eq!(
2268 report.client_order_id.unwrap().to_string(),
2269 "O-19700101-000000-001-001-1"
2270 );
2271 assert_eq!(report.order_side, OrderSide::Buy.into());
2272 assert_eq!(report.order_type, OrderType::Limit);
2273 assert_eq!(report.time_in_force, TimeInForce::Gtc);
2274 assert_eq!(report.order_status, OrderStatus::Accepted);
2275 assert_eq!(report.quantity.as_f64(), 0.0);
2277 assert_eq!(report.filled_qty.as_f64(), 0.0);
2278 assert!(report.post_only);
2279 assert!(!report.reduce_only);
2280 }
2281
2282 #[rstest]
2283 fn test_determine_order_event_type() {
2284 assert_eq!(
2286 determine_order_event_type("open", true, false),
2287 OrderEventType::Accepted
2288 );
2289
2290 assert_eq!(
2292 determine_order_event_type("open", false, true),
2293 OrderEventType::Updated
2294 );
2295
2296 assert_eq!(
2298 determine_order_event_type("cancelled", false, false),
2299 OrderEventType::Canceled
2300 );
2301
2302 assert_eq!(
2304 determine_order_event_type("expired", false, false),
2305 OrderEventType::Expired
2306 );
2307
2308 assert_eq!(
2310 determine_order_event_type("filled", false, false),
2311 OrderEventType::None
2312 );
2313 }
2314}