1use std::str::FromStr;
19
20use anyhow::Context;
21use nautilus_core::{
22 datetime::{NANOSECONDS_IN_MICROSECOND, NANOSECONDS_IN_MILLISECOND},
23 nanos::UnixNanos,
24 uuid::UUID4,
25};
26use nautilus_model::{
27 data::{Bar, BarType, BookOrder, TradeTick},
28 enums::{
29 AccountType, AggressorSide, AssetClass, BookType, CurrencyType, OptionKind, OrderSide,
30 },
31 events::AccountState,
32 identifiers::{AccountId, InstrumentId, Symbol, TradeId, Venue},
33 instruments::{
34 CryptoFuture, CryptoPerpetual, CurrencyPair, OptionContract, any::InstrumentAny,
35 },
36 orderbook::OrderBook,
37 types::{AccountBalance, Currency, MarginBalance, Money, Price, Quantity},
38};
39use rust_decimal::Decimal;
40
41use crate::{
42 common::consts::DERIBIT_VENUE,
43 http::models::{
44 DeribitAccountSummary, DeribitInstrument, DeribitInstrumentKind, DeribitOptionType,
45 DeribitOrderBook, DeribitPublicTrade, DeribitTradingViewChartData,
46 },
47};
48
49#[must_use]
64pub fn parse_instrument_kind_currency(instrument_id: &InstrumentId) -> (String, String) {
65 let symbol = instrument_id.symbol.as_str();
66
67 let kind = if symbol.contains("PERPETUAL") {
70 "future" } else if symbol.ends_with("-C") || symbol.ends_with("-P") {
72 "option"
74 } else if symbol.contains('_') && !symbol.contains('-') {
75 "spot"
77 } else {
78 "future"
80 };
81
82 let currency = if let Some(idx) = symbol.find('-') {
85 let first_part = &symbol[..idx];
88 if let Some(underscore_idx) = first_part.find('_') {
89 first_part[..underscore_idx].to_string()
90 } else {
91 first_part.to_string()
92 }
93 } else if let Some(idx) = symbol.find('_') {
94 symbol[..idx].to_string()
96 } else {
97 "any".to_string()
98 };
99
100 (kind.to_string(), currency)
101}
102
103pub fn extract_server_timestamp(us_out: Option<u64>) -> anyhow::Result<UnixNanos> {
109 let us_out =
110 us_out.ok_or_else(|| anyhow::anyhow!("Missing server timestamp (us_out) in response"))?;
111 Ok(UnixNanos::from(us_out * NANOSECONDS_IN_MICROSECOND))
112}
113
114pub fn parse_deribit_instrument_any(
125 instrument: &DeribitInstrument,
126 ts_init: UnixNanos,
127 ts_event: UnixNanos,
128) -> anyhow::Result<Option<InstrumentAny>> {
129 match instrument.kind {
130 DeribitInstrumentKind::Spot => {
131 parse_spot_instrument(instrument, ts_init, ts_event).map(Some)
132 }
133 DeribitInstrumentKind::Future => {
134 if instrument.instrument_name.as_str().contains("PERPETUAL") {
136 parse_perpetual_instrument(instrument, ts_init, ts_event).map(Some)
137 } else {
138 parse_future_instrument(instrument, ts_init, ts_event).map(Some)
139 }
140 }
141 DeribitInstrumentKind::Option => {
142 parse_option_instrument(instrument, ts_init, ts_event).map(Some)
143 }
144 DeribitInstrumentKind::FutureCombo | DeribitInstrumentKind::OptionCombo => {
145 Ok(None)
147 }
148 }
149}
150
151fn parse_spot_instrument(
153 instrument: &DeribitInstrument,
154 ts_init: UnixNanos,
155 ts_event: UnixNanos,
156) -> anyhow::Result<InstrumentAny> {
157 let instrument_id = InstrumentId::new(Symbol::new(instrument.instrument_name), *DERIBIT_VENUE);
158
159 let base_currency = Currency::new(
160 instrument.base_currency,
161 8,
162 0,
163 instrument.base_currency,
164 CurrencyType::Crypto,
165 );
166 let quote_currency = Currency::new(
167 instrument.quote_currency,
168 8,
169 0,
170 instrument.quote_currency,
171 CurrencyType::Crypto,
172 );
173
174 let price_increment = Price::from(instrument.tick_size.to_string().as_str());
175 let size_increment = Quantity::from(instrument.min_trade_amount.to_string().as_str());
176 let min_quantity = Quantity::from(instrument.min_trade_amount.to_string().as_str());
177
178 let maker_fee = Decimal::from_str(&instrument.maker_commission.to_string())
179 .context("Failed to parse maker_commission")?;
180 let taker_fee = Decimal::from_str(&instrument.taker_commission.to_string())
181 .context("Failed to parse taker_commission")?;
182
183 let currency_pair = CurrencyPair::new(
184 instrument_id,
185 instrument.instrument_name.into(),
186 base_currency,
187 quote_currency,
188 price_increment.precision,
189 size_increment.precision,
190 price_increment,
191 size_increment,
192 None, None, None, Some(min_quantity),
196 None, None, None, None, None, None, Some(maker_fee),
203 Some(taker_fee),
204 ts_event,
205 ts_init,
206 );
207
208 Ok(InstrumentAny::CurrencyPair(currency_pair))
209}
210
211fn parse_perpetual_instrument(
213 instrument: &DeribitInstrument,
214 ts_init: UnixNanos,
215 ts_event: UnixNanos,
216) -> anyhow::Result<InstrumentAny> {
217 let instrument_id = InstrumentId::new(Symbol::new(instrument.instrument_name), *DERIBIT_VENUE);
218
219 let base_currency = Currency::new(
220 instrument.base_currency,
221 8,
222 0,
223 instrument.base_currency,
224 CurrencyType::Crypto,
225 );
226 let quote_currency = Currency::new(
227 instrument.quote_currency,
228 8,
229 0,
230 instrument.quote_currency,
231 CurrencyType::Crypto,
232 );
233 let settlement_currency = instrument.settlement_currency.map_or(base_currency, |c| {
234 Currency::new(c, 8, 0, c, CurrencyType::Crypto)
235 });
236
237 let is_inverse = instrument
238 .instrument_type
239 .as_ref()
240 .is_some_and(|t| t == "reversed");
241
242 let price_increment = Price::from(instrument.tick_size.to_string().as_str());
243 let size_increment = Quantity::from(instrument.min_trade_amount.to_string().as_str());
244 let min_quantity = Quantity::from(instrument.min_trade_amount.to_string().as_str());
245
246 let multiplier = Some(Quantity::from(
248 instrument.contract_size.to_string().as_str(),
249 ));
250 let lot_size = Some(size_increment);
251
252 let maker_fee = Decimal::from_str(&instrument.maker_commission.to_string())
253 .context("Failed to parse maker_commission")?;
254 let taker_fee = Decimal::from_str(&instrument.taker_commission.to_string())
255 .context("Failed to parse taker_commission")?;
256
257 let perpetual = CryptoPerpetual::new(
258 instrument_id,
259 instrument.instrument_name.into(),
260 base_currency,
261 quote_currency,
262 settlement_currency,
263 is_inverse,
264 price_increment.precision,
265 size_increment.precision,
266 price_increment,
267 size_increment,
268 multiplier,
269 lot_size,
270 None, Some(min_quantity),
272 None, None, None, None, None, None, Some(maker_fee),
279 Some(taker_fee),
280 ts_event,
281 ts_init,
282 );
283
284 Ok(InstrumentAny::CryptoPerpetual(perpetual))
285}
286
287fn parse_future_instrument(
289 instrument: &DeribitInstrument,
290 ts_init: UnixNanos,
291 ts_event: UnixNanos,
292) -> anyhow::Result<InstrumentAny> {
293 let instrument_id = InstrumentId::new(Symbol::new(instrument.instrument_name), *DERIBIT_VENUE);
294
295 let underlying = Currency::new(
296 instrument.base_currency,
297 8,
298 0,
299 instrument.base_currency,
300 CurrencyType::Crypto,
301 );
302 let quote_currency = Currency::new(
303 instrument.quote_currency,
304 8,
305 0,
306 instrument.quote_currency,
307 CurrencyType::Crypto,
308 );
309 let settlement_currency = instrument.settlement_currency.map_or(underlying, |c| {
310 Currency::new(c, 8, 0, c, CurrencyType::Crypto)
311 });
312
313 let is_inverse = instrument
314 .instrument_type
315 .as_ref()
316 .is_some_and(|t| t == "reversed");
317
318 let activation_ns = (instrument.creation_timestamp as u64) * 1_000_000;
320 let expiration_ns = instrument
321 .expiration_timestamp
322 .context("Missing expiration_timestamp for future")? as u64
323 * 1_000_000; let price_increment = Price::from(instrument.tick_size.to_string().as_str());
326 let size_increment = Quantity::from(instrument.min_trade_amount.to_string().as_str());
327 let min_quantity = Quantity::from(instrument.min_trade_amount.to_string().as_str());
328
329 let multiplier = Some(Quantity::from(
331 instrument.contract_size.to_string().as_str(),
332 ));
333 let lot_size = Some(size_increment); let maker_fee = Decimal::from_str(&instrument.maker_commission.to_string())
336 .context("Failed to parse maker_commission")?;
337 let taker_fee = Decimal::from_str(&instrument.taker_commission.to_string())
338 .context("Failed to parse taker_commission")?;
339
340 let future = CryptoFuture::new(
341 instrument_id,
342 instrument.instrument_name.into(),
343 underlying,
344 quote_currency,
345 settlement_currency,
346 is_inverse,
347 UnixNanos::from(activation_ns),
348 UnixNanos::from(expiration_ns),
349 price_increment.precision,
350 size_increment.precision,
351 price_increment,
352 size_increment,
353 multiplier,
354 lot_size,
355 None, Some(min_quantity),
357 None, None, None, None, None, None, Some(maker_fee),
364 Some(taker_fee),
365 ts_event,
366 ts_init,
367 );
368
369 Ok(InstrumentAny::CryptoFuture(future))
370}
371
372fn parse_option_instrument(
374 instrument: &DeribitInstrument,
375 ts_init: UnixNanos,
376 ts_event: UnixNanos,
377) -> anyhow::Result<InstrumentAny> {
378 let instrument_id = InstrumentId::new(Symbol::new(instrument.instrument_name), *DERIBIT_VENUE);
379
380 let underlying = instrument.base_currency;
382
383 let settlement = instrument
385 .settlement_currency
386 .unwrap_or(instrument.base_currency);
387 let currency = Currency::new(settlement, 8, 0, settlement, CurrencyType::Crypto);
388
389 let option_kind = match instrument.option_type {
391 Some(DeribitOptionType::Call) => OptionKind::Call,
392 Some(DeribitOptionType::Put) => OptionKind::Put,
393 None => anyhow::bail!("Missing option_type for option instrument"),
394 };
395
396 let strike = instrument.strike.context("Missing strike for option")?;
398 let strike_price = Price::from(strike.to_string().as_str());
399
400 let activation_ns = (instrument.creation_timestamp as u64) * 1_000_000;
402 let expiration_ns = instrument
403 .expiration_timestamp
404 .context("Missing expiration_timestamp for option")? as u64
405 * 1_000_000;
406
407 let price_increment = Price::from(instrument.tick_size.to_string().as_str());
408
409 let multiplier = Quantity::from(instrument.contract_size.to_string().as_str());
411 let lot_size = Quantity::from(instrument.min_trade_amount.to_string().as_str());
412 let min_quantity = Quantity::from(instrument.min_trade_amount.to_string().as_str());
413
414 let maker_fee = Decimal::from_str(&instrument.maker_commission.to_string())
415 .context("Failed to parse maker_commission")?;
416 let taker_fee = Decimal::from_str(&instrument.taker_commission.to_string())
417 .context("Failed to parse taker_commission")?;
418
419 let option = OptionContract::new(
420 instrument_id,
421 instrument.instrument_name.into(),
422 AssetClass::Cryptocurrency,
423 None, underlying,
425 option_kind,
426 strike_price,
427 currency,
428 UnixNanos::from(activation_ns),
429 UnixNanos::from(expiration_ns),
430 price_increment.precision,
431 price_increment,
432 multiplier,
433 lot_size,
434 None, Some(min_quantity),
436 None, None, None, None, Some(maker_fee),
441 Some(taker_fee),
442 ts_event,
443 ts_init,
444 );
445
446 Ok(InstrumentAny::OptionContract(option))
447}
448
449pub fn parse_account_state(
459 summaries: &[DeribitAccountSummary],
460 account_id: AccountId,
461 ts_init: UnixNanos,
462 ts_event: UnixNanos,
463) -> anyhow::Result<AccountState> {
464 let mut balances = Vec::new();
465 let mut margins = Vec::new();
466
467 for summary in summaries {
469 let ccy_str = summary.currency.as_str().trim();
470
471 if ccy_str.is_empty() {
473 log::debug!("Skipping balance detail with empty currency code | raw_data={summary:?}");
474 continue;
475 }
476
477 let currency = Currency::get_or_create_crypto_with_context(
478 ccy_str,
479 Some("DERIBIT - Parsing account state"),
480 );
481
482 let total = Money::new(summary.equity, currency);
485 let free = Money::new(summary.available_funds, currency);
486 let locked = Money::from_raw(total.raw - free.raw, currency);
487
488 let balance = AccountBalance::new(total, locked, free);
489 balances.push(balance);
490
491 if let (Some(initial_margin), Some(maintenance_margin)) =
493 (summary.initial_margin, summary.maintenance_margin)
494 {
495 if initial_margin > 0.0 || maintenance_margin > 0.0 {
497 let initial = Money::new(initial_margin, currency);
498 let maintenance = Money::new(maintenance_margin, currency);
499
500 let margin_instrument_id = InstrumentId::new(
502 Symbol::from_str_unchecked(format!("ACCOUNT-{}", summary.currency)),
503 Venue::new("DERIBIT"),
504 );
505
506 margins.push(MarginBalance::new(
507 initial,
508 maintenance,
509 margin_instrument_id,
510 ));
511 }
512 }
513 }
514
515 if balances.is_empty() {
517 let zero_currency = Currency::USD();
518 let zero_money = Money::new(0.0, zero_currency);
519 let zero_balance = AccountBalance::new(zero_money, zero_money, zero_money);
520 balances.push(zero_balance);
521 }
522
523 let account_type = AccountType::Margin;
524 let is_reported = true;
525
526 Ok(AccountState::new(
527 account_id,
528 account_type,
529 balances,
530 margins,
531 is_reported,
532 UUID4::new(),
533 ts_event,
534 ts_init,
535 None,
536 ))
537}
538
539pub fn parse_trade_tick(
547 trade: &DeribitPublicTrade,
548 instrument_id: InstrumentId,
549 price_precision: u8,
550 size_precision: u8,
551 ts_init: UnixNanos,
552) -> anyhow::Result<TradeTick> {
553 let aggressor_side = match trade.direction.as_str() {
555 "buy" => AggressorSide::Buyer,
556 "sell" => AggressorSide::Seller,
557 other => anyhow::bail!("Invalid trade direction: {other}"),
558 };
559 let price = Price::new(trade.price, price_precision);
560 let size = Quantity::new(trade.amount, size_precision);
561 let ts_event = UnixNanos::from((trade.timestamp as u64) * NANOSECONDS_IN_MILLISECOND);
562 let trade_id = TradeId::new(&trade.trade_id);
563
564 Ok(TradeTick::new(
565 instrument_id,
566 price,
567 size,
568 aggressor_side,
569 trade_id,
570 ts_event,
571 ts_init,
572 ))
573}
574
575pub fn parse_bars(
587 chart_data: &DeribitTradingViewChartData,
588 bar_type: BarType,
589 price_precision: u8,
590 size_precision: u8,
591 ts_init: UnixNanos,
592) -> anyhow::Result<Vec<Bar>> {
593 if chart_data.status != "ok" {
595 anyhow::bail!(
596 "Chart data status is '{}', expected 'ok'",
597 chart_data.status
598 );
599 }
600
601 let num_bars = chart_data.ticks.len();
602
603 anyhow::ensure!(
605 chart_data.open.len() == num_bars
606 && chart_data.high.len() == num_bars
607 && chart_data.low.len() == num_bars
608 && chart_data.close.len() == num_bars
609 && chart_data.volume.len() == num_bars,
610 "Inconsistent array lengths in chart data"
611 );
612
613 if num_bars == 0 {
614 return Ok(Vec::new());
615 }
616
617 let mut bars = Vec::with_capacity(num_bars);
618
619 for i in 0..num_bars {
620 let open = Price::new_checked(chart_data.open[i], price_precision)
621 .with_context(|| format!("Invalid open price at index {i}"))?;
622 let high = Price::new_checked(chart_data.high[i], price_precision)
623 .with_context(|| format!("Invalid high price at index {i}"))?;
624 let low = Price::new_checked(chart_data.low[i], price_precision)
625 .with_context(|| format!("Invalid low price at index {i}"))?;
626 let close = Price::new_checked(chart_data.close[i], price_precision)
627 .with_context(|| format!("Invalid close price at index {i}"))?;
628 let volume = Quantity::new_checked(chart_data.volume[i], size_precision)
629 .with_context(|| format!("Invalid volume at index {i}"))?;
630
631 let ts_event = UnixNanos::from((chart_data.ticks[i] as u64) * NANOSECONDS_IN_MILLISECOND);
633
634 let bar = Bar::new_checked(bar_type, open, high, low, close, volume, ts_event, ts_init)
635 .with_context(|| format!("Invalid OHLC bar at index {i}"))?;
636 bars.push(bar);
637 }
638
639 Ok(bars)
640}
641
642pub fn parse_order_book(
651 order_book_data: &DeribitOrderBook,
652 instrument_id: InstrumentId,
653 price_precision: u8,
654 size_precision: u8,
655 ts_init: UnixNanos,
656) -> anyhow::Result<OrderBook> {
657 let ts_event = UnixNanos::from((order_book_data.timestamp as u64) * NANOSECONDS_IN_MILLISECOND);
658 let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
659
660 for (idx, [price, amount]) in order_book_data.bids.iter().enumerate() {
661 let order = BookOrder::new(
662 OrderSide::Buy,
663 Price::new(*price, price_precision),
664 Quantity::new(*amount, size_precision),
665 idx as u64,
666 );
667 book.add(order, 0, idx as u64, ts_event);
668 }
669
670 let bids_len = order_book_data.bids.len();
671 for (idx, [price, amount]) in order_book_data.asks.iter().enumerate() {
672 let order = BookOrder::new(
673 OrderSide::Sell,
674 Price::new(*price, price_precision),
675 Quantity::new(*amount, size_precision),
676 (bids_len + idx) as u64,
677 );
678 book.add(order, 0, (bids_len + idx) as u64, ts_event);
679 }
680
681 book.ts_last = ts_init;
682
683 Ok(book)
684}
685
686pub fn bar_spec_to_resolution(bar_type: &BarType) -> String {
690 use nautilus_model::enums::BarAggregation;
691
692 let spec = bar_type.spec();
693 match spec.aggregation {
694 BarAggregation::Minute => {
695 let step = spec.step.get();
696 match step {
698 1 => "1".to_string(),
699 2..=3 => "3".to_string(),
700 4..=5 => "5".to_string(),
701 6..=10 => "10".to_string(),
702 11..=15 => "15".to_string(),
703 16..=30 => "30".to_string(),
704 31..=60 => "60".to_string(),
705 61..=120 => "120".to_string(),
706 121..=180 => "180".to_string(),
707 181..=360 => "360".to_string(),
708 361..=720 => "720".to_string(),
709 _ => "1D".to_string(),
710 }
711 }
712 BarAggregation::Hour => {
713 let step = spec.step.get();
714 match step {
715 1 => "60".to_string(),
716 2 => "120".to_string(),
717 3 => "180".to_string(),
718 4..=6 => "360".to_string(),
719 7..=12 => "720".to_string(),
720 _ => "1D".to_string(),
721 }
722 }
723 BarAggregation::Day => "1D".to_string(),
724 _ => {
725 log::warn!(
726 "Unsupported bar aggregation {:?}, defaulting to 1 minute",
727 spec.aggregation
728 );
729 "1".to_string()
730 }
731 }
732}
733
734#[cfg(test)]
735mod tests {
736 use nautilus_model::instruments::Instrument;
737 use rstest::rstest;
738 use rust_decimal_macros::dec;
739
740 use super::*;
741 use crate::{
742 common::testing::load_test_json,
743 http::models::{
744 DeribitAccountSummariesResponse, DeribitJsonRpcResponse, DeribitTradesResponse,
745 },
746 };
747
748 #[rstest]
749 fn test_parse_perpetual_instrument() {
750 let json_data = load_test_json("http_get_instrument.json");
751 let response: DeribitJsonRpcResponse<DeribitInstrument> =
752 serde_json::from_str(&json_data).unwrap();
753 let deribit_inst = response.result.expect("Test data must have result");
754
755 let instrument_any =
756 parse_deribit_instrument_any(&deribit_inst, UnixNanos::default(), UnixNanos::default())
757 .unwrap();
758 let instrument = instrument_any.expect("Should parse perpetual instrument");
759
760 let InstrumentAny::CryptoPerpetual(perpetual) = instrument else {
761 panic!("Expected CryptoPerpetual, got {instrument:?}");
762 };
763 assert_eq!(perpetual.id(), InstrumentId::from("BTC-PERPETUAL.DERIBIT"));
764 assert_eq!(perpetual.raw_symbol(), Symbol::from("BTC-PERPETUAL"));
765 assert_eq!(perpetual.base_currency().unwrap().code, "BTC");
766 assert_eq!(perpetual.quote_currency().code, "USD");
767 assert_eq!(perpetual.settlement_currency().code, "BTC");
768 assert!(perpetual.is_inverse());
769 assert_eq!(perpetual.price_precision(), 1);
770 assert_eq!(perpetual.size_precision(), 0);
771 assert_eq!(perpetual.price_increment(), Price::from("0.5"));
772 assert_eq!(perpetual.size_increment(), Quantity::from("10"));
773 assert_eq!(perpetual.multiplier(), Quantity::from("10"));
774 assert_eq!(perpetual.lot_size(), Some(Quantity::from("10")));
775 assert_eq!(perpetual.maker_fee(), dec!(0));
776 assert_eq!(perpetual.taker_fee(), dec!(0.0005));
777 assert_eq!(perpetual.max_quantity(), None);
778 assert_eq!(perpetual.min_quantity(), Some(Quantity::from("10")));
779 }
780
781 #[rstest]
782 fn test_parse_future_instrument() {
783 let json_data = load_test_json("http_get_instruments.json");
784 let response: DeribitJsonRpcResponse<Vec<DeribitInstrument>> =
785 serde_json::from_str(&json_data).unwrap();
786 let instruments = response.result.expect("Test data must have result");
787 let deribit_inst = instruments
788 .iter()
789 .find(|i| i.instrument_name.as_str() == "BTC-27DEC24")
790 .expect("Test data must contain BTC-27DEC24");
791
792 let instrument_any =
793 parse_deribit_instrument_any(deribit_inst, UnixNanos::default(), UnixNanos::default())
794 .unwrap();
795 let instrument = instrument_any.expect("Should parse future instrument");
796
797 let InstrumentAny::CryptoFuture(future) = instrument else {
798 panic!("Expected CryptoFuture, got {instrument:?}");
799 };
800 assert_eq!(future.id(), InstrumentId::from("BTC-27DEC24.DERIBIT"));
801 assert_eq!(future.raw_symbol(), Symbol::from("BTC-27DEC24"));
802 assert_eq!(future.underlying().unwrap(), "BTC");
803 assert_eq!(future.quote_currency().code, "USD");
804 assert_eq!(future.settlement_currency().code, "BTC");
805 assert!(future.is_inverse());
806
807 assert_eq!(
809 future.activation_ns(),
810 Some(UnixNanos::from(1719561600000_u64 * 1_000_000))
811 );
812 assert_eq!(
813 future.expiration_ns(),
814 Some(UnixNanos::from(1735300800000_u64 * 1_000_000))
815 );
816 assert_eq!(future.price_precision(), 1);
817 assert_eq!(future.size_precision(), 0);
818 assert_eq!(future.price_increment(), Price::from("0.5"));
819 assert_eq!(future.size_increment(), Quantity::from("10"));
820 assert_eq!(future.multiplier(), Quantity::from("10"));
821 assert_eq!(future.lot_size(), Some(Quantity::from("10")));
822 assert_eq!(future.maker_fee, dec!(0));
823 assert_eq!(future.taker_fee, dec!(0.0005));
824 }
825
826 #[rstest]
827 fn test_parse_option_instrument() {
828 let json_data = load_test_json("http_get_instruments.json");
829 let response: DeribitJsonRpcResponse<Vec<DeribitInstrument>> =
830 serde_json::from_str(&json_data).unwrap();
831 let instruments = response.result.expect("Test data must have result");
832 let deribit_inst = instruments
833 .iter()
834 .find(|i| i.instrument_name.as_str() == "BTC-27DEC24-100000-C")
835 .expect("Test data must contain BTC-27DEC24-100000-C");
836
837 let instrument_any =
838 parse_deribit_instrument_any(deribit_inst, UnixNanos::default(), UnixNanos::default())
839 .unwrap();
840 let instrument = instrument_any.expect("Should parse option instrument");
841
842 let InstrumentAny::OptionContract(option) = instrument else {
844 panic!("Expected OptionContract, got {instrument:?}");
845 };
846
847 assert_eq!(
848 option.id(),
849 InstrumentId::from("BTC-27DEC24-100000-C.DERIBIT")
850 );
851 assert_eq!(option.raw_symbol(), Symbol::from("BTC-27DEC24-100000-C"));
852 assert_eq!(option.underlying(), Some("BTC".into()));
853 assert_eq!(option.asset_class(), AssetClass::Cryptocurrency);
854 assert_eq!(option.option_kind(), Some(OptionKind::Call));
855 assert_eq!(option.strike_price(), Some(Price::from("100000")));
856 assert_eq!(option.currency.code, "BTC");
857 assert_eq!(
858 option.activation_ns(),
859 Some(UnixNanos::from(1719561600000_u64 * 1_000_000))
860 );
861 assert_eq!(
862 option.expiration_ns(),
863 Some(UnixNanos::from(1735300800000_u64 * 1_000_000))
864 );
865 assert_eq!(option.price_precision(), 4);
866 assert_eq!(option.price_increment(), Price::from("0.0005"));
867 assert_eq!(option.multiplier(), Quantity::from("1"));
868 assert_eq!(option.lot_size(), Some(Quantity::from("0.1")));
869 assert_eq!(option.maker_fee, dec!(0.0003));
870 assert_eq!(option.taker_fee, dec!(0.0003));
871 }
872
873 #[rstest]
874 fn test_parse_account_state_with_positions() {
875 let json_data = load_test_json("http_get_account_summaries.json");
876 let response: DeribitJsonRpcResponse<DeribitAccountSummariesResponse> =
877 serde_json::from_str(&json_data).unwrap();
878 let result = response.result.expect("Test data must have result");
879
880 let account_id = AccountId::from("DERIBIT-001");
881
882 let ts_event =
884 extract_server_timestamp(response.us_out).expect("Test data must have us_out");
885 let ts_init = UnixNanos::default();
886
887 let account_state = parse_account_state(&result.summaries, account_id, ts_init, ts_event)
888 .expect("Should parse account state");
889
890 assert_eq!(account_state.balances.len(), 2);
892
893 let btc_balance = account_state
895 .balances
896 .iter()
897 .find(|b| b.currency.code == "BTC")
898 .expect("BTC balance should exist");
899
900 assert_eq!(btc_balance.total.as_f64(), 302.61869214);
911 assert_eq!(btc_balance.free.as_f64(), 301.38059622);
912
913 let locked = btc_balance.locked.as_f64();
915 assert!(
916 locked > 0.0,
917 "Locked should be positive when positions exist"
918 );
919 assert!(
920 (locked - 1.24669592).abs() < 0.01,
921 "Locked ({locked}) should be close to initial_margin (1.24669592)"
922 );
923
924 let eth_balance = account_state
926 .balances
927 .iter()
928 .find(|b| b.currency.code == "ETH")
929 .expect("ETH balance should exist");
930
931 assert_eq!(eth_balance.total.as_f64(), 100.0);
936 assert_eq!(eth_balance.free.as_f64(), 99.999598);
937 assert_eq!(eth_balance.locked.as_f64(), 0.000402);
938
939 assert_eq!(account_state.account_id, account_id);
941 assert_eq!(account_state.account_type, AccountType::Margin);
942 assert!(account_state.is_reported);
943
944 let expected_ts_event = UnixNanos::from(1687352432005000_u64 * NANOSECONDS_IN_MICROSECOND);
946 assert_eq!(
947 account_state.ts_event, expected_ts_event,
948 "ts_event should match server timestamp from response"
949 );
950 }
951
952 #[rstest]
953 fn test_parse_trade_tick_sell() {
954 let json_data = load_test_json("http_get_last_trades.json");
955 let response: DeribitJsonRpcResponse<DeribitTradesResponse> =
956 serde_json::from_str(&json_data).unwrap();
957 let result = response.result.expect("Test data must have result");
958
959 assert!(result.has_more, "has_more should be true");
960 assert_eq!(result.trades.len(), 10, "Should have 10 trades");
961
962 let raw_trade = &result.trades[0];
963 let instrument_id = InstrumentId::from("ETH-PERPETUAL.DERIBIT");
964 let ts_init = UnixNanos::from(1766335632425576_u64 * 1000); let trade = parse_trade_tick(raw_trade, instrument_id, 1, 0, ts_init)
967 .expect("Should parse trade tick");
968
969 assert_eq!(trade.instrument_id, instrument_id);
970 assert_eq!(trade.price, Price::from("2968.3"));
971 assert_eq!(trade.size, Quantity::from("1"));
972 assert_eq!(trade.aggressor_side, AggressorSide::Seller);
973 assert_eq!(trade.trade_id, TradeId::new("ETH-284830839"));
974 assert_eq!(
976 trade.ts_event,
977 UnixNanos::from(1766332040636_u64 * 1_000_000)
978 );
979 assert_eq!(trade.ts_init, ts_init);
980 }
981
982 #[rstest]
983 fn test_parse_trade_tick_buy() {
984 let json_data = load_test_json("http_get_last_trades.json");
985 let response: DeribitJsonRpcResponse<DeribitTradesResponse> =
986 serde_json::from_str(&json_data).unwrap();
987 let result = response.result.expect("Test data must have result");
988
989 let raw_trade = &result.trades[9];
991 let instrument_id = InstrumentId::from("ETH-PERPETUAL.DERIBIT");
992 let ts_init = UnixNanos::default();
993
994 let trade = parse_trade_tick(raw_trade, instrument_id, 1, 0, ts_init)
995 .expect("Should parse trade tick");
996
997 assert_eq!(trade.instrument_id, instrument_id);
998 assert_eq!(trade.price, Price::from("2968.3"));
999 assert_eq!(trade.size, Quantity::from("106"));
1000 assert_eq!(trade.aggressor_side, AggressorSide::Buyer);
1001 assert_eq!(trade.trade_id, TradeId::new("ETH-284830854"));
1002 }
1003
1004 #[rstest]
1005 fn test_parse_bars() {
1006 let json_data = load_test_json("http_get_tradingview_chart_data.json");
1007 let response: DeribitJsonRpcResponse<DeribitTradingViewChartData> =
1008 serde_json::from_str(&json_data).unwrap();
1009 let chart_data = response.result.expect("Test data must have result");
1010
1011 let bar_type = BarType::from("BTC-PERPETUAL.DERIBIT-1-MINUTE-LAST-EXTERNAL");
1012 let ts_init = UnixNanos::from(1766487086146245_u64 * NANOSECONDS_IN_MICROSECOND);
1013
1014 let bars = parse_bars(&chart_data, bar_type, 1, 8, ts_init).expect("Should parse bars");
1015
1016 assert_eq!(bars.len(), 5, "Should parse 5 bars");
1017
1018 let first_bar = &bars[0];
1020 assert_eq!(first_bar.bar_type, bar_type);
1021 assert_eq!(first_bar.open, Price::from("87451.0"));
1022 assert_eq!(first_bar.high, Price::from("87456.5"));
1023 assert_eq!(first_bar.low, Price::from("87451.0"));
1024 assert_eq!(first_bar.close, Price::from("87456.5"));
1025 assert_eq!(first_bar.volume, Quantity::from("2.94375216"));
1026 assert_eq!(
1027 first_bar.ts_event,
1028 UnixNanos::from(1766483460000_u64 * NANOSECONDS_IN_MILLISECOND)
1029 );
1030 assert_eq!(first_bar.ts_init, ts_init);
1031
1032 let last_bar = &bars[4];
1034 assert_eq!(last_bar.open, Price::from("87456.0"));
1035 assert_eq!(last_bar.high, Price::from("87456.5"));
1036 assert_eq!(last_bar.low, Price::from("87456.0"));
1037 assert_eq!(last_bar.close, Price::from("87456.0"));
1038 assert_eq!(last_bar.volume, Quantity::from("0.1018798"));
1039 assert_eq!(
1040 last_bar.ts_event,
1041 UnixNanos::from(1766483700000_u64 * NANOSECONDS_IN_MILLISECOND)
1042 );
1043 }
1044
1045 #[rstest]
1046 fn test_parse_order_book() {
1047 let json_data = load_test_json("http_get_order_book.json");
1048 let response: DeribitJsonRpcResponse<DeribitOrderBook> =
1049 serde_json::from_str(&json_data).unwrap();
1050 let order_book_data = response.result.expect("Test data must have result");
1051
1052 let instrument_id = InstrumentId::from("BTC-PERPETUAL.DERIBIT");
1053 let ts_init = UnixNanos::from(1766554855146274_u64 * NANOSECONDS_IN_MICROSECOND);
1054
1055 let book = parse_order_book(&order_book_data, instrument_id, 1, 0, ts_init)
1056 .expect("Should parse order book");
1057
1058 assert_eq!(book.instrument_id, instrument_id);
1060 assert_eq!(book.book_type, BookType::L2_MBP);
1061 assert_eq!(book.ts_last, ts_init);
1062
1063 assert!(book.has_bid(), "Book should have bids");
1065 assert!(book.has_ask(), "Book should have asks");
1066
1067 assert_eq!(
1069 book.best_bid_price(),
1070 Some(Price::from("87002.5")),
1071 "Best bid price should match"
1072 );
1073 assert_eq!(
1074 book.best_bid_size(),
1075 Some(Quantity::from("199190")),
1076 "Best bid size should match"
1077 );
1078
1079 assert_eq!(
1081 book.best_ask_price(),
1082 Some(Price::from("87003.0")),
1083 "Best ask price should match"
1084 );
1085 assert_eq!(
1086 book.best_ask_size(),
1087 Some(Quantity::from("125090")),
1088 "Best ask size should match"
1089 );
1090
1091 let spread = book.spread().expect("Spread should exist");
1093 assert!(
1094 (spread - 0.5).abs() < 0.0001,
1095 "Spread should be 0.5, got {spread}"
1096 );
1097
1098 let midpoint = book.midpoint().expect("Midpoint should exist");
1100 assert!(
1101 (midpoint - 87002.75).abs() < 0.0001,
1102 "Midpoint should be 87002.75, got {midpoint}"
1103 );
1104
1105 let bid_count = book.bids(None).count();
1107 let ask_count = book.asks(None).count();
1108 assert_eq!(
1109 bid_count,
1110 order_book_data.bids.len(),
1111 "Bid levels count should match input data"
1112 );
1113 assert_eq!(
1114 ask_count,
1115 order_book_data.asks.len(),
1116 "Ask levels count should match input data"
1117 );
1118 assert_eq!(bid_count, 20, "Should have 20 bid levels");
1119 assert_eq!(ask_count, 20, "Should have 20 ask levels");
1120
1121 assert_eq!(
1123 book.bids(Some(5)).count(),
1124 5,
1125 "Should limit to 5 bid levels"
1126 );
1127 assert_eq!(
1128 book.asks(Some(5)).count(),
1129 5,
1130 "Should limit to 5 ask levels"
1131 );
1132
1133 let bids_map = book.bids_as_map(None);
1135 let asks_map = book.asks_as_map(None);
1136 assert_eq!(bids_map.len(), 20, "Bids map should have 20 entries");
1137 assert_eq!(asks_map.len(), 20, "Asks map should have 20 entries");
1138
1139 assert!(
1141 bids_map.contains_key(&dec!(87002.5)),
1142 "Bids map should contain best bid price"
1143 );
1144 assert!(
1145 asks_map.contains_key(&dec!(87003.0)),
1146 "Asks map should contain best ask price"
1147 );
1148
1149 assert!(
1151 bids_map.contains_key(&dec!(86980.0)),
1152 "Bids map should contain worst bid price"
1153 );
1154 assert!(
1155 asks_map.contains_key(&dec!(87031.5)),
1156 "Asks map should contain worst ask price"
1157 );
1158 }
1159
1160 fn make_instrument_id(symbol: &str) -> InstrumentId {
1161 InstrumentId::new(Symbol::from(symbol), Venue::from("DERIBIT"))
1162 }
1163
1164 #[rstest]
1165 fn test_parse_futures_and_perpetuals() {
1166 let cases = [
1168 ("BTC-PERPETUAL", "future", "BTC"),
1169 ("ETH-PERPETUAL", "future", "ETH"),
1170 ("SOL-PERPETUAL", "future", "SOL"),
1171 ("BTC-25MAR23", "future", "BTC"),
1173 ("BTC-5AUG23", "future", "BTC"), ("ETH-28MAR25", "future", "ETH"),
1175 ];
1176
1177 for (symbol, expected_kind, expected_currency) in cases {
1178 let (kind, currency) = parse_instrument_kind_currency(&make_instrument_id(symbol));
1179 assert_eq!(kind, expected_kind, "kind mismatch for {symbol}");
1180 assert_eq!(
1181 currency, expected_currency,
1182 "currency mismatch for {symbol}"
1183 );
1184 }
1185 }
1186
1187 #[rstest]
1188 fn test_parse_options() {
1189 let cases = [
1190 ("BTC-25MAR23-420-C", "option", "BTC"),
1192 ("BTC-5AUG23-580-P", "option", "BTC"),
1193 ("ETH-28MAR25-4000-C", "option", "ETH"),
1194 ("XRP_USDC-30JUN23-0d625-C", "option", "XRP"),
1196 ];
1197
1198 for (symbol, expected_kind, expected_currency) in cases {
1199 let (kind, currency) = parse_instrument_kind_currency(&make_instrument_id(symbol));
1200 assert_eq!(kind, expected_kind, "kind mismatch for {symbol}");
1201 assert_eq!(
1202 currency, expected_currency,
1203 "currency mismatch for {symbol}"
1204 );
1205 }
1206 }
1207
1208 #[rstest]
1209 fn test_parse_spot() {
1210 let cases = [
1211 ("BTC_USDC", "spot", "BTC"),
1212 ("ETH_USDT", "spot", "ETH"),
1213 ("SOL_USDC", "spot", "SOL"),
1214 ];
1215
1216 for (symbol, expected_kind, expected_currency) in cases {
1217 let (kind, currency) = parse_instrument_kind_currency(&make_instrument_id(symbol));
1218 assert_eq!(kind, expected_kind, "kind mismatch for {symbol}");
1219 assert_eq!(
1220 currency, expected_currency,
1221 "currency mismatch for {symbol}"
1222 );
1223 }
1224 }
1225}