1use std::str::FromStr;
19
20use anyhow::Context;
21use nautilus_core::{datetime::NANOSECONDS_IN_MICROSECOND, nanos::UnixNanos, uuid::UUID4};
22use nautilus_model::{
23 enums::{AccountType, AssetClass, CurrencyType, OptionKind},
24 events::AccountState,
25 identifiers::{AccountId, InstrumentId, Symbol, Venue},
26 instruments::{
27 CryptoFuture, CryptoPerpetual, CurrencyPair, OptionContract, any::InstrumentAny,
28 },
29 types::{AccountBalance, Currency, MarginBalance, Money, Price, Quantity},
30};
31use rust_decimal::Decimal;
32
33use crate::{
34 common::consts::DERIBIT_VENUE,
35 http::models::{
36 DeribitAccountSummary, DeribitInstrument, DeribitInstrumentKind, DeribitOptionType,
37 },
38};
39
40pub fn extract_server_timestamp(us_out: Option<u64>) -> anyhow::Result<UnixNanos> {
46 let us_out =
47 us_out.ok_or_else(|| anyhow::anyhow!("Missing server timestamp (us_out) in response"))?;
48 Ok(UnixNanos::from(us_out * NANOSECONDS_IN_MICROSECOND))
49}
50
51pub fn parse_deribit_instrument_any(
62 instrument: &DeribitInstrument,
63 ts_init: UnixNanos,
64 ts_event: UnixNanos,
65) -> anyhow::Result<Option<InstrumentAny>> {
66 match instrument.kind {
67 DeribitInstrumentKind::Spot => {
68 parse_spot_instrument(instrument, ts_init, ts_event).map(Some)
69 }
70 DeribitInstrumentKind::Future => {
71 if instrument.instrument_name.as_str().contains("PERPETUAL") {
73 parse_perpetual_instrument(instrument, ts_init, ts_event).map(Some)
74 } else {
75 parse_future_instrument(instrument, ts_init, ts_event).map(Some)
76 }
77 }
78 DeribitInstrumentKind::Option => {
79 parse_option_instrument(instrument, ts_init, ts_event).map(Some)
80 }
81 DeribitInstrumentKind::FutureCombo | DeribitInstrumentKind::OptionCombo => {
82 Ok(None)
84 }
85 }
86}
87
88fn parse_spot_instrument(
90 instrument: &DeribitInstrument,
91 ts_init: UnixNanos,
92 ts_event: UnixNanos,
93) -> anyhow::Result<InstrumentAny> {
94 let instrument_id = InstrumentId::new(Symbol::new(instrument.instrument_name), *DERIBIT_VENUE);
95
96 let base_currency = Currency::new(
97 instrument.base_currency,
98 8,
99 0,
100 instrument.base_currency,
101 CurrencyType::Crypto,
102 );
103 let quote_currency = Currency::new(
104 instrument.quote_currency,
105 8,
106 0,
107 instrument.quote_currency,
108 CurrencyType::Crypto,
109 );
110
111 let price_increment = Price::from(instrument.tick_size.to_string().as_str());
112 let size_increment = Quantity::from(instrument.min_trade_amount.to_string().as_str());
113
114 let maker_fee = Decimal::from_str(&instrument.maker_commission.to_string())
115 .context("Failed to parse maker_commission")?;
116 let taker_fee = Decimal::from_str(&instrument.taker_commission.to_string())
117 .context("Failed to parse taker_commission")?;
118
119 let currency_pair = CurrencyPair::new(
120 instrument_id,
121 instrument.instrument_name.into(),
122 base_currency,
123 quote_currency,
124 price_increment.precision,
125 size_increment.precision,
126 price_increment,
127 size_increment,
128 None, None, None, None, None, None, None, None, None, None, Some(maker_fee),
139 Some(taker_fee),
140 ts_event,
141 ts_init,
142 );
143
144 Ok(InstrumentAny::CurrencyPair(currency_pair))
145}
146
147fn parse_perpetual_instrument(
149 instrument: &DeribitInstrument,
150 ts_init: UnixNanos,
151 ts_event: UnixNanos,
152) -> anyhow::Result<InstrumentAny> {
153 let instrument_id = InstrumentId::new(Symbol::new(instrument.instrument_name), *DERIBIT_VENUE);
154
155 let base_currency = Currency::new(
156 instrument.base_currency,
157 8,
158 0,
159 instrument.base_currency,
160 CurrencyType::Crypto,
161 );
162 let quote_currency = Currency::new(
163 instrument.quote_currency,
164 8,
165 0,
166 instrument.quote_currency,
167 CurrencyType::Crypto,
168 );
169 let settlement_currency = instrument.settlement_currency.map_or(base_currency, |c| {
170 Currency::new(c, 8, 0, c, CurrencyType::Crypto)
171 });
172
173 let is_inverse = instrument
174 .instrument_type
175 .as_ref()
176 .is_some_and(|t| t == "reversed");
177
178 let price_increment = Price::from(instrument.tick_size.to_string().as_str());
179 let size_increment = Quantity::from(instrument.min_trade_amount.to_string().as_str());
180
181 let multiplier = Some(Quantity::from(
183 instrument.contract_size.to_string().as_str(),
184 ));
185 let lot_size = Some(size_increment);
186
187 let maker_fee = Decimal::from_str(&instrument.maker_commission.to_string())
188 .context("Failed to parse maker_commission")?;
189 let taker_fee = Decimal::from_str(&instrument.taker_commission.to_string())
190 .context("Failed to parse taker_commission")?;
191
192 let perpetual = CryptoPerpetual::new(
193 instrument_id,
194 instrument.instrument_name.into(),
195 base_currency,
196 quote_currency,
197 settlement_currency,
198 is_inverse,
199 price_increment.precision,
200 size_increment.precision,
201 price_increment,
202 size_increment,
203 multiplier,
204 lot_size,
205 None, None, None, None, None, None, None, None, Some(maker_fee),
214 Some(taker_fee),
215 ts_event,
216 ts_init,
217 );
218
219 Ok(InstrumentAny::CryptoPerpetual(perpetual))
220}
221
222fn parse_future_instrument(
224 instrument: &DeribitInstrument,
225 ts_init: UnixNanos,
226 ts_event: UnixNanos,
227) -> anyhow::Result<InstrumentAny> {
228 let instrument_id = InstrumentId::new(Symbol::new(instrument.instrument_name), *DERIBIT_VENUE);
229
230 let underlying = Currency::new(
231 instrument.base_currency,
232 8,
233 0,
234 instrument.base_currency,
235 CurrencyType::Crypto,
236 );
237 let quote_currency = Currency::new(
238 instrument.quote_currency,
239 8,
240 0,
241 instrument.quote_currency,
242 CurrencyType::Crypto,
243 );
244 let settlement_currency = instrument.settlement_currency.map_or(underlying, |c| {
245 Currency::new(c, 8, 0, c, CurrencyType::Crypto)
246 });
247
248 let is_inverse = instrument
249 .instrument_type
250 .as_ref()
251 .is_some_and(|t| t == "reversed");
252
253 let activation_ns = (instrument.creation_timestamp as u64) * 1_000_000;
255 let expiration_ns = instrument
256 .expiration_timestamp
257 .context("Missing expiration_timestamp for future")? as u64
258 * 1_000_000; let price_increment = Price::from(instrument.tick_size.to_string().as_str());
261 let size_increment = Quantity::from(instrument.min_trade_amount.to_string().as_str());
262
263 let multiplier = Some(Quantity::from(
265 instrument.contract_size.to_string().as_str(),
266 ));
267 let lot_size = Some(size_increment); let maker_fee = Decimal::from_str(&instrument.maker_commission.to_string())
270 .context("Failed to parse maker_commission")?;
271 let taker_fee = Decimal::from_str(&instrument.taker_commission.to_string())
272 .context("Failed to parse taker_commission")?;
273
274 let future = CryptoFuture::new(
275 instrument_id,
276 instrument.instrument_name.into(),
277 underlying,
278 quote_currency,
279 settlement_currency,
280 is_inverse,
281 UnixNanos::from(activation_ns),
282 UnixNanos::from(expiration_ns),
283 price_increment.precision,
284 size_increment.precision,
285 price_increment,
286 size_increment,
287 multiplier,
288 lot_size,
289 None, None, None, None, None, None, None, None, Some(maker_fee),
298 Some(taker_fee),
299 ts_event,
300 ts_init,
301 );
302
303 Ok(InstrumentAny::CryptoFuture(future))
304}
305
306fn parse_option_instrument(
308 instrument: &DeribitInstrument,
309 ts_init: UnixNanos,
310 ts_event: UnixNanos,
311) -> anyhow::Result<InstrumentAny> {
312 let instrument_id = InstrumentId::new(Symbol::new(instrument.instrument_name), *DERIBIT_VENUE);
313
314 let underlying = instrument.base_currency;
316
317 let settlement = instrument
319 .settlement_currency
320 .unwrap_or(instrument.base_currency);
321 let currency = Currency::new(settlement, 8, 0, settlement, CurrencyType::Crypto);
322
323 let option_kind = match instrument.option_type {
325 Some(DeribitOptionType::Call) => OptionKind::Call,
326 Some(DeribitOptionType::Put) => OptionKind::Put,
327 None => anyhow::bail!("Missing option_type for option instrument"),
328 };
329
330 let strike = instrument.strike.context("Missing strike for option")?;
332 let strike_price = Price::from(strike.to_string().as_str());
333
334 let activation_ns = (instrument.creation_timestamp as u64) * 1_000_000;
336 let expiration_ns = instrument
337 .expiration_timestamp
338 .context("Missing expiration_timestamp for option")? as u64
339 * 1_000_000;
340
341 let price_increment = Price::from(instrument.tick_size.to_string().as_str());
342
343 let multiplier = Quantity::from(instrument.contract_size.to_string().as_str());
345 let lot_size = Quantity::from(instrument.min_trade_amount.to_string().as_str());
346
347 let maker_fee = Decimal::from_str(&instrument.maker_commission.to_string())
348 .context("Failed to parse maker_commission")?;
349 let taker_fee = Decimal::from_str(&instrument.taker_commission.to_string())
350 .context("Failed to parse taker_commission")?;
351
352 let option = OptionContract::new(
353 instrument_id,
354 instrument.instrument_name.into(),
355 AssetClass::Cryptocurrency,
356 None, underlying,
358 option_kind,
359 strike_price,
360 currency,
361 UnixNanos::from(activation_ns),
362 UnixNanos::from(expiration_ns),
363 price_increment.precision,
364 price_increment,
365 multiplier,
366 lot_size,
367 None, None, None, None, None, None, Some(maker_fee),
374 Some(taker_fee),
375 ts_event,
376 ts_init,
377 );
378
379 Ok(InstrumentAny::OptionContract(option))
380}
381
382pub fn parse_account_state(
392 summaries: &[DeribitAccountSummary],
393 account_id: AccountId,
394 ts_init: UnixNanos,
395 ts_event: UnixNanos,
396) -> anyhow::Result<AccountState> {
397 let mut balances = Vec::new();
398 let mut margins = Vec::new();
399
400 for summary in summaries {
402 let ccy_str = summary.currency.as_str().trim();
403
404 if ccy_str.is_empty() {
406 tracing::debug!(
407 "Skipping balance detail with empty currency code | raw_data={:?}",
408 summary
409 );
410 continue;
411 }
412
413 let currency = Currency::get_or_create_crypto_with_context(
414 ccy_str,
415 Some("DERIBIT - Parsing account state"),
416 );
417
418 let total = Money::new(summary.equity, currency);
421 let free = Money::new(summary.available_funds, currency);
422 let locked = Money::from_raw(total.raw - free.raw, currency);
423
424 let balance = AccountBalance::new(total, locked, free);
425 balances.push(balance);
426
427 if let (Some(initial_margin), Some(maintenance_margin)) =
429 (summary.initial_margin, summary.maintenance_margin)
430 {
431 if initial_margin > 0.0 || maintenance_margin > 0.0 {
433 let initial = Money::new(initial_margin, currency);
434 let maintenance = Money::new(maintenance_margin, currency);
435
436 let margin_instrument_id = InstrumentId::new(
438 Symbol::from_str_unchecked(format!("ACCOUNT-{}", summary.currency)),
439 Venue::new("DERIBIT"),
440 );
441
442 margins.push(MarginBalance::new(
443 initial,
444 maintenance,
445 margin_instrument_id,
446 ));
447 }
448 }
449 }
450
451 if balances.is_empty() {
453 let zero_currency = Currency::USD();
454 let zero_money = Money::new(0.0, zero_currency);
455 let zero_balance = AccountBalance::new(zero_money, zero_money, zero_money);
456 balances.push(zero_balance);
457 }
458
459 let account_type = AccountType::Margin;
460 let is_reported = true;
461
462 Ok(AccountState::new(
463 account_id,
464 account_type,
465 balances,
466 margins,
467 is_reported,
468 UUID4::new(),
469 ts_event,
470 ts_init,
471 None,
472 ))
473}
474
475#[cfg(test)]
476mod tests {
477 use nautilus_model::instruments::Instrument;
478 use rstest::rstest;
479 use rust_decimal_macros::dec;
480
481 use super::*;
482 use crate::{
483 common::testing::load_test_json,
484 http::models::{DeribitAccountSummariesResponse, DeribitJsonRpcResponse},
485 };
486
487 #[rstest]
488 fn test_parse_perpetual_instrument() {
489 let json_data = load_test_json("http_get_instrument.json");
490 let response: DeribitJsonRpcResponse<DeribitInstrument> =
491 serde_json::from_str(&json_data).unwrap();
492 let deribit_inst = response.result.expect("Test data must have result");
493
494 let instrument_any =
495 parse_deribit_instrument_any(&deribit_inst, UnixNanos::default(), UnixNanos::default())
496 .unwrap();
497 let instrument = instrument_any.expect("Should parse perpetual instrument");
498
499 let InstrumentAny::CryptoPerpetual(perpetual) = instrument else {
500 panic!("Expected CryptoPerpetual, got {instrument:?}");
501 };
502 assert_eq!(perpetual.id(), InstrumentId::from("BTC-PERPETUAL.DERIBIT"));
503 assert_eq!(perpetual.raw_symbol(), Symbol::from("BTC-PERPETUAL"));
504 assert_eq!(perpetual.base_currency().unwrap().code, "BTC");
505 assert_eq!(perpetual.quote_currency().code, "USD");
506 assert_eq!(perpetual.settlement_currency().code, "BTC");
507 assert!(perpetual.is_inverse());
508 assert_eq!(perpetual.price_precision(), 1);
509 assert_eq!(perpetual.size_precision(), 0);
510 assert_eq!(perpetual.price_increment(), Price::from("0.5"));
511 assert_eq!(perpetual.size_increment(), Quantity::from("10"));
512 assert_eq!(perpetual.multiplier(), Quantity::from("10"));
513 assert_eq!(perpetual.lot_size(), Some(Quantity::from("10")));
514 assert_eq!(perpetual.maker_fee(), dec!(0));
515 assert_eq!(perpetual.taker_fee(), dec!(0.0005));
516 assert_eq!(perpetual.max_quantity(), None);
517 assert_eq!(perpetual.min_quantity(), None);
518 }
519
520 #[rstest]
521 fn test_parse_future_instrument() {
522 let json_data = load_test_json("http_get_instruments.json");
523 let response: DeribitJsonRpcResponse<Vec<DeribitInstrument>> =
524 serde_json::from_str(&json_data).unwrap();
525 let instruments = response.result.expect("Test data must have result");
526 let deribit_inst = instruments
527 .iter()
528 .find(|i| i.instrument_name.as_str() == "BTC-27DEC24")
529 .expect("Test data must contain BTC-27DEC24");
530
531 let instrument_any =
532 parse_deribit_instrument_any(deribit_inst, UnixNanos::default(), UnixNanos::default())
533 .unwrap();
534 let instrument = instrument_any.expect("Should parse future instrument");
535
536 let InstrumentAny::CryptoFuture(future) = instrument else {
537 panic!("Expected CryptoFuture, got {instrument:?}");
538 };
539 assert_eq!(future.id(), InstrumentId::from("BTC-27DEC24.DERIBIT"));
540 assert_eq!(future.raw_symbol(), Symbol::from("BTC-27DEC24"));
541 assert_eq!(future.underlying().unwrap(), "BTC");
542 assert_eq!(future.quote_currency().code, "USD");
543 assert_eq!(future.settlement_currency().code, "BTC");
544 assert!(future.is_inverse());
545
546 assert_eq!(
548 future.activation_ns(),
549 Some(UnixNanos::from(1719561600000_u64 * 1_000_000))
550 );
551 assert_eq!(
552 future.expiration_ns(),
553 Some(UnixNanos::from(1735300800000_u64 * 1_000_000))
554 );
555 assert_eq!(future.price_precision(), 1);
556 assert_eq!(future.size_precision(), 0);
557 assert_eq!(future.price_increment(), Price::from("0.5"));
558 assert_eq!(future.size_increment(), Quantity::from("10"));
559 assert_eq!(future.multiplier(), Quantity::from("10"));
560 assert_eq!(future.lot_size(), Some(Quantity::from("10")));
561 assert_eq!(future.maker_fee, dec!(0));
562 assert_eq!(future.taker_fee, dec!(0.0005));
563 }
564
565 #[rstest]
566 fn test_parse_option_instrument() {
567 let json_data = load_test_json("http_get_instruments.json");
568 let response: DeribitJsonRpcResponse<Vec<DeribitInstrument>> =
569 serde_json::from_str(&json_data).unwrap();
570 let instruments = response.result.expect("Test data must have result");
571 let deribit_inst = instruments
572 .iter()
573 .find(|i| i.instrument_name.as_str() == "BTC-27DEC24-100000-C")
574 .expect("Test data must contain BTC-27DEC24-100000-C");
575
576 let instrument_any =
577 parse_deribit_instrument_any(deribit_inst, UnixNanos::default(), UnixNanos::default())
578 .unwrap();
579 let instrument = instrument_any.expect("Should parse option instrument");
580
581 let InstrumentAny::OptionContract(option) = instrument else {
583 panic!("Expected OptionContract, got {instrument:?}");
584 };
585
586 assert_eq!(
587 option.id(),
588 InstrumentId::from("BTC-27DEC24-100000-C.DERIBIT")
589 );
590 assert_eq!(option.raw_symbol(), Symbol::from("BTC-27DEC24-100000-C"));
591 assert_eq!(option.underlying(), Some("BTC".into()));
592 assert_eq!(option.asset_class(), AssetClass::Cryptocurrency);
593 assert_eq!(option.option_kind(), Some(OptionKind::Call));
594 assert_eq!(option.strike_price(), Some(Price::from("100000")));
595 assert_eq!(option.currency.code, "BTC");
596 assert_eq!(
597 option.activation_ns(),
598 Some(UnixNanos::from(1719561600000_u64 * 1_000_000))
599 );
600 assert_eq!(
601 option.expiration_ns(),
602 Some(UnixNanos::from(1735300800000_u64 * 1_000_000))
603 );
604 assert_eq!(option.price_precision(), 4);
605 assert_eq!(option.price_increment(), Price::from("0.0005"));
606 assert_eq!(option.multiplier(), Quantity::from("1"));
607 assert_eq!(option.lot_size(), Some(Quantity::from("0.1")));
608 assert_eq!(option.maker_fee, dec!(0.0003));
609 assert_eq!(option.taker_fee, dec!(0.0003));
610 }
611
612 #[rstest]
613 fn test_parse_account_state_with_positions() {
614 let json_data = load_test_json("http_get_account_summaries.json");
615 let response: DeribitJsonRpcResponse<DeribitAccountSummariesResponse> =
616 serde_json::from_str(&json_data).unwrap();
617 let result = response.result.expect("Test data must have result");
618
619 let account_id = AccountId::from("DERIBIT-001");
620
621 let ts_event =
623 extract_server_timestamp(response.us_out).expect("Test data must have us_out");
624 let ts_init = UnixNanos::default();
625
626 let account_state = parse_account_state(&result.summaries, account_id, ts_init, ts_event)
627 .expect("Should parse account state");
628
629 assert_eq!(account_state.balances.len(), 2);
631
632 let btc_balance = account_state
634 .balances
635 .iter()
636 .find(|b| b.currency.code == "BTC")
637 .expect("BTC balance should exist");
638
639 assert_eq!(btc_balance.total.as_f64(), 302.61869214);
650 assert_eq!(btc_balance.free.as_f64(), 301.38059622);
651
652 let locked = btc_balance.locked.as_f64();
654 assert!(
655 locked > 0.0,
656 "Locked should be positive when positions exist"
657 );
658 assert!(
659 (locked - 1.24669592).abs() < 0.01,
660 "Locked ({locked}) should be close to initial_margin (1.24669592)"
661 );
662
663 let eth_balance = account_state
665 .balances
666 .iter()
667 .find(|b| b.currency.code == "ETH")
668 .expect("ETH balance should exist");
669
670 assert_eq!(eth_balance.total.as_f64(), 100.0);
675 assert_eq!(eth_balance.free.as_f64(), 99.999598);
676 assert_eq!(eth_balance.locked.as_f64(), 0.000402);
677
678 assert_eq!(account_state.account_id, account_id);
680 assert_eq!(account_state.account_type, AccountType::Margin);
681 assert!(account_state.is_reported);
682
683 let expected_ts_event = UnixNanos::from(1687352432005000_u64 * NANOSECONDS_IN_MICROSECOND);
685 assert_eq!(
686 account_state.ts_event, expected_ts_event,
687 "ts_event should match server timestamp from response"
688 );
689 }
690}