Skip to main content

nautilus_deribit/
data_types.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Deribit-specific custom data types.
17//!
18//! These types carry Deribit domain data through the Nautilus data engine as
19//! [`CustomData`](nautilus_model::data::CustomData).
20
21use nautilus_core::UnixNanos;
22use nautilus_model::{
23    custom_data,
24    identifiers::{InstrumentId, Symbol},
25};
26#[cfg(feature = "arrow")]
27use nautilus_serialization::arrow_custom_data;
28use rust_decimal::Decimal;
29
30use crate::{common::consts::DERIBIT_VENUE, http::models::DeribitBookSummaryRaw};
31
32/// Deribit volatility index (DVOL) update.
33///
34/// Emitted from the `deribit_volatility_index.{index_name}` WebSocket channel.
35#[cfg_attr(
36    feature = "arrow",
37    arrow_custom_data(pyo3, stub_module = "nautilus_trader.adapters.deribit")
38)]
39#[custom_data(pyo3, stub_module = "nautilus_trader.adapters.deribit")]
40pub struct DeribitVolatilityIndex {
41    /// The index identifier (for example `"btc_usd"` or `"eth_usd"`).
42    pub index_name: String,
43    /// The DVOL value for `index_name`.
44    pub volatility: f64,
45    /// UNIX timestamp (nanoseconds) when the data event occurred.
46    pub ts_event: UnixNanos,
47    /// UNIX timestamp (nanoseconds) when the instance was initialized.
48    pub ts_init: UnixNanos,
49}
50
51/// Book summary snapshot for one instrument from
52/// `public/get_book_summary_by_currency`.
53///
54/// Numeric venue fields are retained as [`Decimal`] (no `f64` round-trip).
55/// Convert from the wire DTO via [`DeribitBookSummary::from_raw`].
56#[cfg_attr(
57    feature = "arrow",
58    arrow_custom_data(pyo3, stub_module = "nautilus_trader.adapters.deribit")
59)]
60#[custom_data(pyo3, stub_module = "nautilus_trader.adapters.deribit")]
61pub struct DeribitBookSummary {
62    /// Nautilus instrument identifier (venue-qualified).
63    pub instrument_id: InstrumentId,
64    /// Venue instrument name (e.g. `"BTC-28MAR25-90000-C"`).
65    pub instrument_name: String,
66    /// Forward/underlying price used for IV calculations.
67    #[custom_data_field(serde)]
68    pub underlying_price: Option<Decimal>,
69    /// Underlying future or index name.
70    #[custom_data_field(serde)]
71    pub underlying_index: Option<String>,
72    /// Mark price.
73    #[custom_data_field(serde)]
74    pub mark_price: Option<Decimal>,
75    /// Mid price.
76    #[custom_data_field(serde)]
77    pub mid_price: Option<Decimal>,
78    /// Best bid price.
79    #[custom_data_field(serde)]
80    pub bid_price: Option<Decimal>,
81    /// Best ask price.
82    #[custom_data_field(serde)]
83    pub ask_price: Option<Decimal>,
84    /// Last traded price.
85    #[custom_data_field(serde)]
86    pub last_price: Option<Decimal>,
87    /// Mark implied volatility.
88    #[custom_data_field(serde)]
89    pub mark_iv: Option<Decimal>,
90    /// Bid implied volatility.
91    #[custom_data_field(serde)]
92    pub bid_iv: Option<Decimal>,
93    /// Ask implied volatility.
94    #[custom_data_field(serde)]
95    pub ask_iv: Option<Decimal>,
96    /// Interest rate used in IV calculations.
97    #[custom_data_field(serde)]
98    pub interest_rate: Option<Decimal>,
99    /// Open interest.
100    #[custom_data_field(serde)]
101    pub open_interest: Option<Decimal>,
102    /// Open interest value when provided.
103    #[custom_data_field(serde)]
104    pub open_interest_value: Option<Decimal>,
105    /// 24h volume.
106    #[custom_data_field(serde)]
107    pub volume: Option<Decimal>,
108    /// 24h volume in USD.
109    #[custom_data_field(serde)]
110    pub volume_usd: Option<Decimal>,
111    /// 24h notional volume.
112    #[custom_data_field(serde)]
113    pub volume_notional: Option<Decimal>,
114    /// 24h volume in BTC when provided.
115    #[custom_data_field(serde)]
116    pub volume_btc: Option<Decimal>,
117    /// 24h high.
118    #[custom_data_field(serde)]
119    pub high: Option<Decimal>,
120    /// 24h low.
121    #[custom_data_field(serde)]
122    pub low: Option<Decimal>,
123    /// 24h price change.
124    #[custom_data_field(serde)]
125    pub price_change: Option<Decimal>,
126    /// Estimated delivery price.
127    #[custom_data_field(serde)]
128    pub estimated_delivery_price: Option<Decimal>,
129    /// Settlement/delivery price when present.
130    #[custom_data_field(serde)]
131    pub delivery_price: Option<Decimal>,
132    /// Base currency.
133    #[custom_data_field(serde)]
134    pub base_currency: Option<String>,
135    /// Quote currency.
136    #[custom_data_field(serde)]
137    pub quote_currency: Option<String>,
138    /// Book summary snapshot time (milliseconds since UNIX epoch).
139    pub creation_timestamp: i64,
140    /// UNIX timestamp (nanoseconds) when the venue generated the summary.
141    pub ts_event: UnixNanos,
142    /// UNIX timestamp (nanoseconds) when the instance was initialized.
143    pub ts_init: UnixNanos,
144}
145
146impl DeribitBookSummary {
147    /// Builds a domain book summary from a venue wire DTO.
148    #[must_use]
149    pub fn from_raw(raw: DeribitBookSummaryRaw, ts_init: UnixNanos) -> Self {
150        let instrument_id = InstrumentId::new(Symbol::new(&raw.instrument_name), *DERIBIT_VENUE);
151        let ts_event = UnixNanos::from_millis(raw.creation_timestamp as u64);
152
153        Self {
154            instrument_id,
155            instrument_name: raw.instrument_name,
156            underlying_price: raw.underlying_price,
157            underlying_index: raw.underlying_index,
158            mark_price: raw.mark_price,
159            mid_price: raw.mid_price,
160            bid_price: raw.bid_price,
161            ask_price: raw.ask_price,
162            last_price: raw.last_price,
163            mark_iv: raw.mark_iv,
164            bid_iv: raw.bid_iv,
165            ask_iv: raw.ask_iv,
166            interest_rate: raw.interest_rate,
167            open_interest: raw.open_interest,
168            open_interest_value: raw.open_interest_value,
169            volume: raw.volume,
170            volume_usd: raw.volume_usd,
171            volume_notional: raw.volume_notional,
172            volume_btc: raw.volume_btc,
173            high: raw.high,
174            low: raw.low,
175            price_change: raw.price_change,
176            estimated_delivery_price: raw.estimated_delivery_price,
177            delivery_price: raw.delivery_price,
178            base_currency: raw.base_currency,
179            quote_currency: raw.quote_currency,
180            creation_timestamp: raw.creation_timestamp,
181            ts_event,
182            ts_init,
183        }
184    }
185}
186
187/// Registers Deribit custom data types.
188///
189/// Safe to call multiple times (idempotent via internal `Once` guards).
190pub fn register_deribit_custom_data() {
191    #[cfg(feature = "arrow")]
192    {
193        nautilus_serialization::ensure_custom_data_registered::<DeribitVolatilityIndex>();
194        nautilus_serialization::ensure_custom_data_registered::<DeribitBookSummary>();
195    }
196
197    #[cfg(not(feature = "arrow"))]
198    {
199        let _ =
200            nautilus_model::data::ensure_custom_data_json_registered::<DeribitVolatilityIndex>();
201        let _ = nautilus_model::data::ensure_custom_data_json_registered::<DeribitBookSummary>();
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use rstest::rstest;
208    use rust_decimal_macros::dec;
209
210    use super::*;
211
212    #[rstest]
213    fn test_register_deribit_custom_data_is_idempotent() {
214        register_deribit_custom_data();
215        register_deribit_custom_data();
216    }
217
218    #[rstest]
219    fn test_book_summary_from_raw_preserves_decimals_and_instrument_id() {
220        let raw = DeribitBookSummaryRaw {
221            instrument_name: "BTC-28MAR25-90000-C".to_string(),
222            underlying_price: Some(dec!(95000.5)),
223            underlying_index: Some("SYN.BTC-28MAR25".to_string()),
224            mark_price: Some(dec!(0.042)),
225            mid_price: None,
226            bid_price: Some(dec!(0.040)),
227            ask_price: Some(dec!(0.042)),
228            last_price: None,
229            mark_iv: Some(dec!(55.2)),
230            bid_iv: None,
231            ask_iv: None,
232            interest_rate: None,
233            open_interest: Some(dec!(123.5)),
234            open_interest_value: None,
235            volume: None,
236            volume_usd: None,
237            volume_notional: None,
238            volume_btc: None,
239            high: None,
240            low: None,
241            price_change: None,
242            estimated_delivery_price: None,
243            delivery_price: None,
244            base_currency: Some("BTC".to_string()),
245            quote_currency: Some("USD".to_string()),
246            creation_timestamp: 1_710_000_000_000,
247        };
248        let ts = UnixNanos::from(42_u64);
249        let summary = DeribitBookSummary::from_raw(raw, ts);
250
251        assert_eq!(
252            summary.instrument_id,
253            InstrumentId::from("BTC-28MAR25-90000-C.DERIBIT")
254        );
255        assert_eq!(summary.mark_iv, Some(dec!(55.2)));
256        assert_eq!(summary.open_interest, Some(dec!(123.5)));
257        assert_eq!(summary.ts_event, UnixNanos::from_millis(1_710_000_000_000));
258        assert_eq!(summary.ts_init, ts);
259    }
260
261    #[cfg(feature = "arrow")]
262    #[rstest]
263    fn test_deribit_volatility_index_arrow_schema() {
264        use arrow::datatypes::DataType;
265        use nautilus_serialization::arrow::{ArrowSchemaProvider, timestamp_data_type};
266
267        let schema = DeribitVolatilityIndex::get_schema(None);
268
269        assert_eq!(schema.fields().len(), 4);
270        assert_eq!(schema.field(0).name(), "index_name");
271        assert_eq!(schema.field(0).data_type(), &DataType::Utf8);
272        assert_eq!(schema.field(1).name(), "volatility");
273        assert_eq!(schema.field(1).data_type(), &DataType::Float64);
274        assert_eq!(schema.field(2).name(), "ts_event");
275        assert_eq!(schema.field(2).data_type(), &timestamp_data_type());
276        assert_eq!(schema.field(3).name(), "ts_init");
277        assert_eq!(schema.field(3).data_type(), &timestamp_data_type());
278    }
279
280    #[cfg(feature = "arrow")]
281    #[rstest]
282    fn test_book_summary_arrow_roundtrip_preserves_decimals() {
283        use arrow::datatypes::DataType;
284        use nautilus_serialization::arrow::{
285            ArrowSchemaProvider, DecodeDataFromRecordBatch, EncodeToRecordBatch,
286        };
287
288        let original = DeribitBookSummary::from_raw(
289            DeribitBookSummaryRaw {
290                instrument_name: "BTC-28MAR25-90000-C".to_string(),
291                underlying_price: Some(dec!(95000.5)),
292                underlying_index: Some("SYN.BTC-28MAR25".to_string()),
293                mark_price: Some(dec!(0.042)),
294                mid_price: None,
295                bid_price: Some(dec!(0.040)),
296                ask_price: Some(dec!(0.042)),
297                last_price: None,
298                mark_iv: Some(dec!(55.2)),
299                bid_iv: None,
300                ask_iv: None,
301                interest_rate: None,
302                open_interest: Some(dec!(123.5)),
303                open_interest_value: None,
304                volume: None,
305                volume_usd: None,
306                volume_notional: None,
307                volume_btc: None,
308                high: None,
309                low: None,
310                price_change: None,
311                estimated_delivery_price: None,
312                delivery_price: None,
313                base_currency: Some("BTC".to_string()),
314                quote_currency: Some("USD".to_string()),
315                creation_timestamp: 1_710_000_000_000,
316            },
317            UnixNanos::from(1_000_u64),
318        );
319
320        // Built-in #[custom_data_field(serde)] path: Option<Decimal> → Utf8.
321        let schema = DeribitBookSummary::get_schema(None);
322        assert_eq!(
323            schema.field_with_name("mark_iv").unwrap().data_type(),
324            &DataType::Utf8
325        );
326
327        let metadata = original.metadata();
328        let batch =
329            DeribitBookSummary::encode_batch(&metadata, std::slice::from_ref(&original)).unwrap();
330        let decoded = DeribitBookSummary::decode_data_batch(&metadata, batch).unwrap();
331        let decoded = DeribitBookSummary::try_from(decoded.into_iter().next().unwrap()).unwrap();
332        assert_eq!(decoded, original);
333    }
334}