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
49pub fn extract_server_timestamp(us_out: Option<u64>) -> anyhow::Result<UnixNanos> {
55 let us_out =
56 us_out.ok_or_else(|| anyhow::anyhow!("Missing server timestamp (us_out) in response"))?;
57 Ok(UnixNanos::from(us_out * NANOSECONDS_IN_MICROSECOND))
58}
59
60pub fn parse_deribit_instrument_any(
71 instrument: &DeribitInstrument,
72 ts_init: UnixNanos,
73 ts_event: UnixNanos,
74) -> anyhow::Result<Option<InstrumentAny>> {
75 match instrument.kind {
76 DeribitInstrumentKind::Spot => {
77 parse_spot_instrument(instrument, ts_init, ts_event).map(Some)
78 }
79 DeribitInstrumentKind::Future => {
80 if instrument.instrument_name.as_str().contains("PERPETUAL") {
82 parse_perpetual_instrument(instrument, ts_init, ts_event).map(Some)
83 } else {
84 parse_future_instrument(instrument, ts_init, ts_event).map(Some)
85 }
86 }
87 DeribitInstrumentKind::Option => {
88 parse_option_instrument(instrument, ts_init, ts_event).map(Some)
89 }
90 DeribitInstrumentKind::FutureCombo | DeribitInstrumentKind::OptionCombo => {
91 Ok(None)
93 }
94 }
95}
96
97fn parse_spot_instrument(
99 instrument: &DeribitInstrument,
100 ts_init: UnixNanos,
101 ts_event: UnixNanos,
102) -> anyhow::Result<InstrumentAny> {
103 let instrument_id = InstrumentId::new(Symbol::new(instrument.instrument_name), *DERIBIT_VENUE);
104
105 let base_currency = Currency::new(
106 instrument.base_currency,
107 8,
108 0,
109 instrument.base_currency,
110 CurrencyType::Crypto,
111 );
112 let quote_currency = Currency::new(
113 instrument.quote_currency,
114 8,
115 0,
116 instrument.quote_currency,
117 CurrencyType::Crypto,
118 );
119
120 let price_increment = Price::from(instrument.tick_size.to_string().as_str());
121 let size_increment = Quantity::from(instrument.min_trade_amount.to_string().as_str());
122 let min_quantity = Quantity::from(instrument.min_trade_amount.to_string().as_str());
123
124 let maker_fee = Decimal::from_str(&instrument.maker_commission.to_string())
125 .context("Failed to parse maker_commission")?;
126 let taker_fee = Decimal::from_str(&instrument.taker_commission.to_string())
127 .context("Failed to parse taker_commission")?;
128
129 let currency_pair = CurrencyPair::new(
130 instrument_id,
131 instrument.instrument_name.into(),
132 base_currency,
133 quote_currency,
134 price_increment.precision,
135 size_increment.precision,
136 price_increment,
137 size_increment,
138 None, None, None, Some(min_quantity),
142 None, None, None, None, None, None, Some(maker_fee),
149 Some(taker_fee),
150 ts_event,
151 ts_init,
152 );
153
154 Ok(InstrumentAny::CurrencyPair(currency_pair))
155}
156
157fn parse_perpetual_instrument(
159 instrument: &DeribitInstrument,
160 ts_init: UnixNanos,
161 ts_event: UnixNanos,
162) -> anyhow::Result<InstrumentAny> {
163 let instrument_id = InstrumentId::new(Symbol::new(instrument.instrument_name), *DERIBIT_VENUE);
164
165 let base_currency = Currency::new(
166 instrument.base_currency,
167 8,
168 0,
169 instrument.base_currency,
170 CurrencyType::Crypto,
171 );
172 let quote_currency = Currency::new(
173 instrument.quote_currency,
174 8,
175 0,
176 instrument.quote_currency,
177 CurrencyType::Crypto,
178 );
179 let settlement_currency = instrument.settlement_currency.map_or(base_currency, |c| {
180 Currency::new(c, 8, 0, c, CurrencyType::Crypto)
181 });
182
183 let is_inverse = instrument
184 .instrument_type
185 .as_ref()
186 .is_some_and(|t| t == "reversed");
187
188 let price_increment = Price::from(instrument.tick_size.to_string().as_str());
189 let size_increment = Quantity::from(instrument.min_trade_amount.to_string().as_str());
190 let min_quantity = Quantity::from(instrument.min_trade_amount.to_string().as_str());
191
192 let multiplier = Some(Quantity::from(
194 instrument.contract_size.to_string().as_str(),
195 ));
196 let lot_size = Some(size_increment);
197
198 let maker_fee = Decimal::from_str(&instrument.maker_commission.to_string())
199 .context("Failed to parse maker_commission")?;
200 let taker_fee = Decimal::from_str(&instrument.taker_commission.to_string())
201 .context("Failed to parse taker_commission")?;
202
203 let perpetual = CryptoPerpetual::new(
204 instrument_id,
205 instrument.instrument_name.into(),
206 base_currency,
207 quote_currency,
208 settlement_currency,
209 is_inverse,
210 price_increment.precision,
211 size_increment.precision,
212 price_increment,
213 size_increment,
214 multiplier,
215 lot_size,
216 None, Some(min_quantity),
218 None, None, None, None, None, None, Some(maker_fee),
225 Some(taker_fee),
226 ts_event,
227 ts_init,
228 );
229
230 Ok(InstrumentAny::CryptoPerpetual(perpetual))
231}
232
233fn parse_future_instrument(
235 instrument: &DeribitInstrument,
236 ts_init: UnixNanos,
237 ts_event: UnixNanos,
238) -> anyhow::Result<InstrumentAny> {
239 let instrument_id = InstrumentId::new(Symbol::new(instrument.instrument_name), *DERIBIT_VENUE);
240
241 let underlying = Currency::new(
242 instrument.base_currency,
243 8,
244 0,
245 instrument.base_currency,
246 CurrencyType::Crypto,
247 );
248 let quote_currency = Currency::new(
249 instrument.quote_currency,
250 8,
251 0,
252 instrument.quote_currency,
253 CurrencyType::Crypto,
254 );
255 let settlement_currency = instrument.settlement_currency.map_or(underlying, |c| {
256 Currency::new(c, 8, 0, c, CurrencyType::Crypto)
257 });
258
259 let is_inverse = instrument
260 .instrument_type
261 .as_ref()
262 .is_some_and(|t| t == "reversed");
263
264 let activation_ns = (instrument.creation_timestamp as u64) * 1_000_000;
266 let expiration_ns = instrument
267 .expiration_timestamp
268 .context("Missing expiration_timestamp for future")? as u64
269 * 1_000_000; let price_increment = Price::from(instrument.tick_size.to_string().as_str());
272 let size_increment = Quantity::from(instrument.min_trade_amount.to_string().as_str());
273 let min_quantity = Quantity::from(instrument.min_trade_amount.to_string().as_str());
274
275 let multiplier = Some(Quantity::from(
277 instrument.contract_size.to_string().as_str(),
278 ));
279 let lot_size = Some(size_increment); let maker_fee = Decimal::from_str(&instrument.maker_commission.to_string())
282 .context("Failed to parse maker_commission")?;
283 let taker_fee = Decimal::from_str(&instrument.taker_commission.to_string())
284 .context("Failed to parse taker_commission")?;
285
286 let future = CryptoFuture::new(
287 instrument_id,
288 instrument.instrument_name.into(),
289 underlying,
290 quote_currency,
291 settlement_currency,
292 is_inverse,
293 UnixNanos::from(activation_ns),
294 UnixNanos::from(expiration_ns),
295 price_increment.precision,
296 size_increment.precision,
297 price_increment,
298 size_increment,
299 multiplier,
300 lot_size,
301 None, Some(min_quantity),
303 None, None, None, None, None, None, Some(maker_fee),
310 Some(taker_fee),
311 ts_event,
312 ts_init,
313 );
314
315 Ok(InstrumentAny::CryptoFuture(future))
316}
317
318fn parse_option_instrument(
320 instrument: &DeribitInstrument,
321 ts_init: UnixNanos,
322 ts_event: UnixNanos,
323) -> anyhow::Result<InstrumentAny> {
324 let instrument_id = InstrumentId::new(Symbol::new(instrument.instrument_name), *DERIBIT_VENUE);
325
326 let underlying = instrument.base_currency;
328
329 let settlement = instrument
331 .settlement_currency
332 .unwrap_or(instrument.base_currency);
333 let currency = Currency::new(settlement, 8, 0, settlement, CurrencyType::Crypto);
334
335 let option_kind = match instrument.option_type {
337 Some(DeribitOptionType::Call) => OptionKind::Call,
338 Some(DeribitOptionType::Put) => OptionKind::Put,
339 None => anyhow::bail!("Missing option_type for option instrument"),
340 };
341
342 let strike = instrument.strike.context("Missing strike for option")?;
344 let strike_price = Price::from(strike.to_string().as_str());
345
346 let activation_ns = (instrument.creation_timestamp as u64) * 1_000_000;
348 let expiration_ns = instrument
349 .expiration_timestamp
350 .context("Missing expiration_timestamp for option")? as u64
351 * 1_000_000;
352
353 let price_increment = Price::from(instrument.tick_size.to_string().as_str());
354
355 let multiplier = Quantity::from(instrument.contract_size.to_string().as_str());
357 let lot_size = Quantity::from(instrument.min_trade_amount.to_string().as_str());
358 let min_quantity = Quantity::from(instrument.min_trade_amount.to_string().as_str());
359
360 let maker_fee = Decimal::from_str(&instrument.maker_commission.to_string())
361 .context("Failed to parse maker_commission")?;
362 let taker_fee = Decimal::from_str(&instrument.taker_commission.to_string())
363 .context("Failed to parse taker_commission")?;
364
365 let option = OptionContract::new(
366 instrument_id,
367 instrument.instrument_name.into(),
368 AssetClass::Cryptocurrency,
369 None, underlying,
371 option_kind,
372 strike_price,
373 currency,
374 UnixNanos::from(activation_ns),
375 UnixNanos::from(expiration_ns),
376 price_increment.precision,
377 price_increment,
378 multiplier,
379 lot_size,
380 None, Some(min_quantity),
382 None, None, None, None, Some(maker_fee),
387 Some(taker_fee),
388 ts_event,
389 ts_init,
390 );
391
392 Ok(InstrumentAny::OptionContract(option))
393}
394
395pub fn parse_account_state(
405 summaries: &[DeribitAccountSummary],
406 account_id: AccountId,
407 ts_init: UnixNanos,
408 ts_event: UnixNanos,
409) -> anyhow::Result<AccountState> {
410 let mut balances = Vec::new();
411 let mut margins = Vec::new();
412
413 for summary in summaries {
415 let ccy_str = summary.currency.as_str().trim();
416
417 if ccy_str.is_empty() {
419 tracing::debug!(
420 "Skipping balance detail with empty currency code | raw_data={:?}",
421 summary
422 );
423 continue;
424 }
425
426 let currency = Currency::get_or_create_crypto_with_context(
427 ccy_str,
428 Some("DERIBIT - Parsing account state"),
429 );
430
431 let total = Money::new(summary.equity, currency);
434 let free = Money::new(summary.available_funds, currency);
435 let locked = Money::from_raw(total.raw - free.raw, currency);
436
437 let balance = AccountBalance::new(total, locked, free);
438 balances.push(balance);
439
440 if let (Some(initial_margin), Some(maintenance_margin)) =
442 (summary.initial_margin, summary.maintenance_margin)
443 {
444 if initial_margin > 0.0 || maintenance_margin > 0.0 {
446 let initial = Money::new(initial_margin, currency);
447 let maintenance = Money::new(maintenance_margin, currency);
448
449 let margin_instrument_id = InstrumentId::new(
451 Symbol::from_str_unchecked(format!("ACCOUNT-{}", summary.currency)),
452 Venue::new("DERIBIT"),
453 );
454
455 margins.push(MarginBalance::new(
456 initial,
457 maintenance,
458 margin_instrument_id,
459 ));
460 }
461 }
462 }
463
464 if balances.is_empty() {
466 let zero_currency = Currency::USD();
467 let zero_money = Money::new(0.0, zero_currency);
468 let zero_balance = AccountBalance::new(zero_money, zero_money, zero_money);
469 balances.push(zero_balance);
470 }
471
472 let account_type = AccountType::Margin;
473 let is_reported = true;
474
475 Ok(AccountState::new(
476 account_id,
477 account_type,
478 balances,
479 margins,
480 is_reported,
481 UUID4::new(),
482 ts_event,
483 ts_init,
484 None,
485 ))
486}
487
488pub fn parse_trade_tick(
496 trade: &DeribitPublicTrade,
497 instrument_id: InstrumentId,
498 price_precision: u8,
499 size_precision: u8,
500 ts_init: UnixNanos,
501) -> anyhow::Result<TradeTick> {
502 let aggressor_side = match trade.direction.as_str() {
504 "buy" => AggressorSide::Buyer,
505 "sell" => AggressorSide::Seller,
506 other => anyhow::bail!("Invalid trade direction: {other}"),
507 };
508 let price = Price::new(trade.price, price_precision);
509 let size = Quantity::new(trade.amount, size_precision);
510 let ts_event = UnixNanos::from((trade.timestamp as u64) * NANOSECONDS_IN_MILLISECOND);
511 let trade_id = TradeId::new(&trade.trade_id);
512
513 Ok(TradeTick::new(
514 instrument_id,
515 price,
516 size,
517 aggressor_side,
518 trade_id,
519 ts_event,
520 ts_init,
521 ))
522}
523
524pub fn parse_bars(
536 chart_data: &DeribitTradingViewChartData,
537 bar_type: BarType,
538 price_precision: u8,
539 size_precision: u8,
540 ts_init: UnixNanos,
541) -> anyhow::Result<Vec<Bar>> {
542 if chart_data.status != "ok" {
544 anyhow::bail!(
545 "Chart data status is '{}', expected 'ok'",
546 chart_data.status
547 );
548 }
549
550 let num_bars = chart_data.ticks.len();
551
552 anyhow::ensure!(
554 chart_data.open.len() == num_bars
555 && chart_data.high.len() == num_bars
556 && chart_data.low.len() == num_bars
557 && chart_data.close.len() == num_bars
558 && chart_data.volume.len() == num_bars,
559 "Inconsistent array lengths in chart data"
560 );
561
562 if num_bars == 0 {
563 return Ok(Vec::new());
564 }
565
566 let mut bars = Vec::with_capacity(num_bars);
567
568 for i in 0..num_bars {
569 let open = Price::new_checked(chart_data.open[i], price_precision)
570 .with_context(|| format!("Invalid open price at index {i}"))?;
571 let high = Price::new_checked(chart_data.high[i], price_precision)
572 .with_context(|| format!("Invalid high price at index {i}"))?;
573 let low = Price::new_checked(chart_data.low[i], price_precision)
574 .with_context(|| format!("Invalid low price at index {i}"))?;
575 let close = Price::new_checked(chart_data.close[i], price_precision)
576 .with_context(|| format!("Invalid close price at index {i}"))?;
577 let volume = Quantity::new_checked(chart_data.volume[i], size_precision)
578 .with_context(|| format!("Invalid volume at index {i}"))?;
579
580 let ts_event = UnixNanos::from((chart_data.ticks[i] as u64) * NANOSECONDS_IN_MILLISECOND);
582
583 let bar = Bar::new_checked(bar_type, open, high, low, close, volume, ts_event, ts_init)
584 .with_context(|| format!("Invalid OHLC bar at index {i}"))?;
585 bars.push(bar);
586 }
587
588 Ok(bars)
589}
590
591pub fn parse_order_book(
600 order_book_data: &DeribitOrderBook,
601 instrument_id: InstrumentId,
602 price_precision: u8,
603 size_precision: u8,
604 ts_init: UnixNanos,
605) -> anyhow::Result<OrderBook> {
606 let ts_event = UnixNanos::from((order_book_data.timestamp as u64) * NANOSECONDS_IN_MILLISECOND);
607 let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
608
609 for (idx, [price, amount]) in order_book_data.bids.iter().enumerate() {
610 let order = BookOrder::new(
611 OrderSide::Buy,
612 Price::new(*price, price_precision),
613 Quantity::new(*amount, size_precision),
614 idx as u64,
615 );
616 book.add(order, 0, idx as u64, ts_event);
617 }
618
619 let bids_len = order_book_data.bids.len();
620 for (idx, [price, amount]) in order_book_data.asks.iter().enumerate() {
621 let order = BookOrder::new(
622 OrderSide::Sell,
623 Price::new(*price, price_precision),
624 Quantity::new(*amount, size_precision),
625 (bids_len + idx) as u64,
626 );
627 book.add(order, 0, (bids_len + idx) as u64, ts_event);
628 }
629
630 book.ts_last = ts_init;
631
632 Ok(book)
633}
634
635#[cfg(test)]
636mod tests {
637 use nautilus_model::instruments::Instrument;
638 use rstest::rstest;
639 use rust_decimal_macros::dec;
640
641 use super::*;
642 use crate::{
643 common::testing::load_test_json,
644 http::models::{
645 DeribitAccountSummariesResponse, DeribitJsonRpcResponse, DeribitTradesResponse,
646 },
647 };
648
649 #[rstest]
650 fn test_parse_perpetual_instrument() {
651 let json_data = load_test_json("http_get_instrument.json");
652 let response: DeribitJsonRpcResponse<DeribitInstrument> =
653 serde_json::from_str(&json_data).unwrap();
654 let deribit_inst = response.result.expect("Test data must have result");
655
656 let instrument_any =
657 parse_deribit_instrument_any(&deribit_inst, UnixNanos::default(), UnixNanos::default())
658 .unwrap();
659 let instrument = instrument_any.expect("Should parse perpetual instrument");
660
661 let InstrumentAny::CryptoPerpetual(perpetual) = instrument else {
662 panic!("Expected CryptoPerpetual, got {instrument:?}");
663 };
664 assert_eq!(perpetual.id(), InstrumentId::from("BTC-PERPETUAL.DERIBIT"));
665 assert_eq!(perpetual.raw_symbol(), Symbol::from("BTC-PERPETUAL"));
666 assert_eq!(perpetual.base_currency().unwrap().code, "BTC");
667 assert_eq!(perpetual.quote_currency().code, "USD");
668 assert_eq!(perpetual.settlement_currency().code, "BTC");
669 assert!(perpetual.is_inverse());
670 assert_eq!(perpetual.price_precision(), 1);
671 assert_eq!(perpetual.size_precision(), 0);
672 assert_eq!(perpetual.price_increment(), Price::from("0.5"));
673 assert_eq!(perpetual.size_increment(), Quantity::from("10"));
674 assert_eq!(perpetual.multiplier(), Quantity::from("10"));
675 assert_eq!(perpetual.lot_size(), Some(Quantity::from("10")));
676 assert_eq!(perpetual.maker_fee(), dec!(0));
677 assert_eq!(perpetual.taker_fee(), dec!(0.0005));
678 assert_eq!(perpetual.max_quantity(), None);
679 assert_eq!(perpetual.min_quantity(), Some(Quantity::from("10")));
680 }
681
682 #[rstest]
683 fn test_parse_future_instrument() {
684 let json_data = load_test_json("http_get_instruments.json");
685 let response: DeribitJsonRpcResponse<Vec<DeribitInstrument>> =
686 serde_json::from_str(&json_data).unwrap();
687 let instruments = response.result.expect("Test data must have result");
688 let deribit_inst = instruments
689 .iter()
690 .find(|i| i.instrument_name.as_str() == "BTC-27DEC24")
691 .expect("Test data must contain BTC-27DEC24");
692
693 let instrument_any =
694 parse_deribit_instrument_any(deribit_inst, UnixNanos::default(), UnixNanos::default())
695 .unwrap();
696 let instrument = instrument_any.expect("Should parse future instrument");
697
698 let InstrumentAny::CryptoFuture(future) = instrument else {
699 panic!("Expected CryptoFuture, got {instrument:?}");
700 };
701 assert_eq!(future.id(), InstrumentId::from("BTC-27DEC24.DERIBIT"));
702 assert_eq!(future.raw_symbol(), Symbol::from("BTC-27DEC24"));
703 assert_eq!(future.underlying().unwrap(), "BTC");
704 assert_eq!(future.quote_currency().code, "USD");
705 assert_eq!(future.settlement_currency().code, "BTC");
706 assert!(future.is_inverse());
707
708 assert_eq!(
710 future.activation_ns(),
711 Some(UnixNanos::from(1719561600000_u64 * 1_000_000))
712 );
713 assert_eq!(
714 future.expiration_ns(),
715 Some(UnixNanos::from(1735300800000_u64 * 1_000_000))
716 );
717 assert_eq!(future.price_precision(), 1);
718 assert_eq!(future.size_precision(), 0);
719 assert_eq!(future.price_increment(), Price::from("0.5"));
720 assert_eq!(future.size_increment(), Quantity::from("10"));
721 assert_eq!(future.multiplier(), Quantity::from("10"));
722 assert_eq!(future.lot_size(), Some(Quantity::from("10")));
723 assert_eq!(future.maker_fee, dec!(0));
724 assert_eq!(future.taker_fee, dec!(0.0005));
725 }
726
727 #[rstest]
728 fn test_parse_option_instrument() {
729 let json_data = load_test_json("http_get_instruments.json");
730 let response: DeribitJsonRpcResponse<Vec<DeribitInstrument>> =
731 serde_json::from_str(&json_data).unwrap();
732 let instruments = response.result.expect("Test data must have result");
733 let deribit_inst = instruments
734 .iter()
735 .find(|i| i.instrument_name.as_str() == "BTC-27DEC24-100000-C")
736 .expect("Test data must contain BTC-27DEC24-100000-C");
737
738 let instrument_any =
739 parse_deribit_instrument_any(deribit_inst, UnixNanos::default(), UnixNanos::default())
740 .unwrap();
741 let instrument = instrument_any.expect("Should parse option instrument");
742
743 let InstrumentAny::OptionContract(option) = instrument else {
745 panic!("Expected OptionContract, got {instrument:?}");
746 };
747
748 assert_eq!(
749 option.id(),
750 InstrumentId::from("BTC-27DEC24-100000-C.DERIBIT")
751 );
752 assert_eq!(option.raw_symbol(), Symbol::from("BTC-27DEC24-100000-C"));
753 assert_eq!(option.underlying(), Some("BTC".into()));
754 assert_eq!(option.asset_class(), AssetClass::Cryptocurrency);
755 assert_eq!(option.option_kind(), Some(OptionKind::Call));
756 assert_eq!(option.strike_price(), Some(Price::from("100000")));
757 assert_eq!(option.currency.code, "BTC");
758 assert_eq!(
759 option.activation_ns(),
760 Some(UnixNanos::from(1719561600000_u64 * 1_000_000))
761 );
762 assert_eq!(
763 option.expiration_ns(),
764 Some(UnixNanos::from(1735300800000_u64 * 1_000_000))
765 );
766 assert_eq!(option.price_precision(), 4);
767 assert_eq!(option.price_increment(), Price::from("0.0005"));
768 assert_eq!(option.multiplier(), Quantity::from("1"));
769 assert_eq!(option.lot_size(), Some(Quantity::from("0.1")));
770 assert_eq!(option.maker_fee, dec!(0.0003));
771 assert_eq!(option.taker_fee, dec!(0.0003));
772 }
773
774 #[rstest]
775 fn test_parse_account_state_with_positions() {
776 let json_data = load_test_json("http_get_account_summaries.json");
777 let response: DeribitJsonRpcResponse<DeribitAccountSummariesResponse> =
778 serde_json::from_str(&json_data).unwrap();
779 let result = response.result.expect("Test data must have result");
780
781 let account_id = AccountId::from("DERIBIT-001");
782
783 let ts_event =
785 extract_server_timestamp(response.us_out).expect("Test data must have us_out");
786 let ts_init = UnixNanos::default();
787
788 let account_state = parse_account_state(&result.summaries, account_id, ts_init, ts_event)
789 .expect("Should parse account state");
790
791 assert_eq!(account_state.balances.len(), 2);
793
794 let btc_balance = account_state
796 .balances
797 .iter()
798 .find(|b| b.currency.code == "BTC")
799 .expect("BTC balance should exist");
800
801 assert_eq!(btc_balance.total.as_f64(), 302.61869214);
812 assert_eq!(btc_balance.free.as_f64(), 301.38059622);
813
814 let locked = btc_balance.locked.as_f64();
816 assert!(
817 locked > 0.0,
818 "Locked should be positive when positions exist"
819 );
820 assert!(
821 (locked - 1.24669592).abs() < 0.01,
822 "Locked ({locked}) should be close to initial_margin (1.24669592)"
823 );
824
825 let eth_balance = account_state
827 .balances
828 .iter()
829 .find(|b| b.currency.code == "ETH")
830 .expect("ETH balance should exist");
831
832 assert_eq!(eth_balance.total.as_f64(), 100.0);
837 assert_eq!(eth_balance.free.as_f64(), 99.999598);
838 assert_eq!(eth_balance.locked.as_f64(), 0.000402);
839
840 assert_eq!(account_state.account_id, account_id);
842 assert_eq!(account_state.account_type, AccountType::Margin);
843 assert!(account_state.is_reported);
844
845 let expected_ts_event = UnixNanos::from(1687352432005000_u64 * NANOSECONDS_IN_MICROSECOND);
847 assert_eq!(
848 account_state.ts_event, expected_ts_event,
849 "ts_event should match server timestamp from response"
850 );
851 }
852
853 #[rstest]
854 fn test_parse_trade_tick_sell() {
855 let json_data = load_test_json("http_get_last_trades.json");
856 let response: DeribitJsonRpcResponse<DeribitTradesResponse> =
857 serde_json::from_str(&json_data).unwrap();
858 let result = response.result.expect("Test data must have result");
859
860 assert!(result.has_more, "has_more should be true");
861 assert_eq!(result.trades.len(), 10, "Should have 10 trades");
862
863 let raw_trade = &result.trades[0];
864 let instrument_id = InstrumentId::from("ETH-PERPETUAL.DERIBIT");
865 let ts_init = UnixNanos::from(1766335632425576_u64 * 1000); let trade = parse_trade_tick(raw_trade, instrument_id, 1, 0, ts_init)
868 .expect("Should parse trade tick");
869
870 assert_eq!(trade.instrument_id, instrument_id);
871 assert_eq!(trade.price, Price::from("2968.3"));
872 assert_eq!(trade.size, Quantity::from("1"));
873 assert_eq!(trade.aggressor_side, AggressorSide::Seller);
874 assert_eq!(trade.trade_id, TradeId::new("ETH-284830839"));
875 assert_eq!(
877 trade.ts_event,
878 UnixNanos::from(1766332040636_u64 * 1_000_000)
879 );
880 assert_eq!(trade.ts_init, ts_init);
881 }
882
883 #[rstest]
884 fn test_parse_trade_tick_buy() {
885 let json_data = load_test_json("http_get_last_trades.json");
886 let response: DeribitJsonRpcResponse<DeribitTradesResponse> =
887 serde_json::from_str(&json_data).unwrap();
888 let result = response.result.expect("Test data must have result");
889
890 let raw_trade = &result.trades[9];
892 let instrument_id = InstrumentId::from("ETH-PERPETUAL.DERIBIT");
893 let ts_init = UnixNanos::default();
894
895 let trade = parse_trade_tick(raw_trade, instrument_id, 1, 0, ts_init)
896 .expect("Should parse trade tick");
897
898 assert_eq!(trade.instrument_id, instrument_id);
899 assert_eq!(trade.price, Price::from("2968.3"));
900 assert_eq!(trade.size, Quantity::from("106"));
901 assert_eq!(trade.aggressor_side, AggressorSide::Buyer);
902 assert_eq!(trade.trade_id, TradeId::new("ETH-284830854"));
903 }
904
905 #[rstest]
906 fn test_parse_bars() {
907 let json_data = load_test_json("http_get_tradingview_chart_data.json");
908 let response: DeribitJsonRpcResponse<DeribitTradingViewChartData> =
909 serde_json::from_str(&json_data).unwrap();
910 let chart_data = response.result.expect("Test data must have result");
911
912 let bar_type = BarType::from("BTC-PERPETUAL.DERIBIT-1-MINUTE-LAST-EXTERNAL");
913 let ts_init = UnixNanos::from(1766487086146245_u64 * NANOSECONDS_IN_MICROSECOND);
914
915 let bars = parse_bars(&chart_data, bar_type, 1, 8, ts_init).expect("Should parse bars");
916
917 assert_eq!(bars.len(), 5, "Should parse 5 bars");
918
919 let first_bar = &bars[0];
921 assert_eq!(first_bar.bar_type, bar_type);
922 assert_eq!(first_bar.open, Price::from("87451.0"));
923 assert_eq!(first_bar.high, Price::from("87456.5"));
924 assert_eq!(first_bar.low, Price::from("87451.0"));
925 assert_eq!(first_bar.close, Price::from("87456.5"));
926 assert_eq!(first_bar.volume, Quantity::from("2.94375216"));
927 assert_eq!(
928 first_bar.ts_event,
929 UnixNanos::from(1766483460000_u64 * NANOSECONDS_IN_MILLISECOND)
930 );
931 assert_eq!(first_bar.ts_init, ts_init);
932
933 let last_bar = &bars[4];
935 assert_eq!(last_bar.open, Price::from("87456.0"));
936 assert_eq!(last_bar.high, Price::from("87456.5"));
937 assert_eq!(last_bar.low, Price::from("87456.0"));
938 assert_eq!(last_bar.close, Price::from("87456.0"));
939 assert_eq!(last_bar.volume, Quantity::from("0.1018798"));
940 assert_eq!(
941 last_bar.ts_event,
942 UnixNanos::from(1766483700000_u64 * NANOSECONDS_IN_MILLISECOND)
943 );
944 }
945
946 #[rstest]
947 fn test_parse_order_book() {
948 let json_data = load_test_json("http_get_order_book.json");
949 let response: DeribitJsonRpcResponse<DeribitOrderBook> =
950 serde_json::from_str(&json_data).unwrap();
951 let order_book_data = response.result.expect("Test data must have result");
952
953 let instrument_id = InstrumentId::from("BTC-PERPETUAL.DERIBIT");
954 let ts_init = UnixNanos::from(1766554855146274_u64 * NANOSECONDS_IN_MICROSECOND);
955
956 let book = parse_order_book(&order_book_data, instrument_id, 1, 0, ts_init)
957 .expect("Should parse order book");
958
959 assert_eq!(book.instrument_id, instrument_id);
961 assert_eq!(book.book_type, BookType::L2_MBP);
962 assert_eq!(book.ts_last, ts_init);
963
964 assert!(book.has_bid(), "Book should have bids");
966 assert!(book.has_ask(), "Book should have asks");
967
968 assert_eq!(
970 book.best_bid_price(),
971 Some(Price::from("87002.5")),
972 "Best bid price should match"
973 );
974 assert_eq!(
975 book.best_bid_size(),
976 Some(Quantity::from("199190")),
977 "Best bid size should match"
978 );
979
980 assert_eq!(
982 book.best_ask_price(),
983 Some(Price::from("87003.0")),
984 "Best ask price should match"
985 );
986 assert_eq!(
987 book.best_ask_size(),
988 Some(Quantity::from("125090")),
989 "Best ask size should match"
990 );
991
992 let spread = book.spread().expect("Spread should exist");
994 assert!(
995 (spread - 0.5).abs() < 0.0001,
996 "Spread should be 0.5, got {spread}"
997 );
998
999 let midpoint = book.midpoint().expect("Midpoint should exist");
1001 assert!(
1002 (midpoint - 87002.75).abs() < 0.0001,
1003 "Midpoint should be 87002.75, got {midpoint}"
1004 );
1005
1006 let bid_count = book.bids(None).count();
1008 let ask_count = book.asks(None).count();
1009 assert_eq!(
1010 bid_count,
1011 order_book_data.bids.len(),
1012 "Bid levels count should match input data"
1013 );
1014 assert_eq!(
1015 ask_count,
1016 order_book_data.asks.len(),
1017 "Ask levels count should match input data"
1018 );
1019 assert_eq!(bid_count, 20, "Should have 20 bid levels");
1020 assert_eq!(ask_count, 20, "Should have 20 ask levels");
1021
1022 assert_eq!(
1024 book.bids(Some(5)).count(),
1025 5,
1026 "Should limit to 5 bid levels"
1027 );
1028 assert_eq!(
1029 book.asks(Some(5)).count(),
1030 5,
1031 "Should limit to 5 ask levels"
1032 );
1033
1034 let bids_map = book.bids_as_map(None);
1036 let asks_map = book.asks_as_map(None);
1037 assert_eq!(bids_map.len(), 20, "Bids map should have 20 entries");
1038 assert_eq!(asks_map.len(), 20, "Asks map should have 20 entries");
1039
1040 assert!(
1042 bids_map.contains_key(&dec!(87002.5)),
1043 "Bids map should contain best bid price"
1044 );
1045 assert!(
1046 asks_map.contains_key(&dec!(87003.0)),
1047 "Asks map should contain best ask price"
1048 );
1049
1050 assert!(
1052 bids_map.contains_key(&dec!(86980.0)),
1053 "Bids map should contain worst bid price"
1054 );
1055 assert!(
1056 asks_map.contains_key(&dec!(87031.5)),
1057 "Asks map should contain worst ask price"
1058 );
1059 }
1060}