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::identifiers::{InstrumentId, Symbol};
23use nautilus_persistence_macros::custom_data;
24use rust_decimal::Decimal;
25
26use crate::{common::consts::DERIBIT_VENUE, http::models::DeribitBookSummaryRaw};
27
28/// Deribit volatility index (DVOL) update.
29///
30/// Emitted from the `deribit_volatility_index.{index_name}` WebSocket channel.
31#[cfg_attr(
32    feature = "arrow",
33    custom_data(pyo3, stub_module = "nautilus_trader.adapters.deribit")
34)]
35#[cfg_attr(
36    not(feature = "arrow"),
37    custom_data(pyo3, no_arrow, stub_module = "nautilus_trader.adapters.deribit")
38)]
39pub struct DeribitVolatilityIndex {
40    /// The index identifier (for example `"btc_usd"` or `"eth_usd"`).
41    pub index_name: String,
42    /// The DVOL value for `index_name`.
43    pub volatility: f64,
44    /// UNIX timestamp (nanoseconds) when the data event occurred.
45    pub ts_event: UnixNanos,
46    /// UNIX timestamp (nanoseconds) when the instance was initialized.
47    pub ts_init: UnixNanos,
48}
49
50/// Book summary snapshot for one instrument from
51/// `public/get_book_summary_by_currency`.
52///
53/// Numeric venue fields are retained as [`Decimal`] (no `f64` round-trip).
54/// Convert from the wire DTO via [`DeribitBookSummary::from_raw`].
55#[cfg_attr(
56    feature = "arrow",
57    custom_data(pyo3, stub_module = "nautilus_trader.adapters.deribit")
58)]
59#[cfg_attr(
60    not(feature = "arrow"),
61    custom_data(pyo3, no_arrow, stub_module = "nautilus_trader.adapters.deribit")
62)]
63pub struct DeribitBookSummary {
64    /// Nautilus instrument identifier (venue-qualified).
65    pub instrument_id: InstrumentId,
66    /// Venue instrument name (e.g. `"BTC-28MAR25-90000-C"`).
67    pub instrument_name: String,
68    /// Forward/underlying price used for IV calculations.
69    #[custom_data_field(serde)]
70    pub underlying_price: Option<Decimal>,
71    /// Underlying future or index name.
72    #[custom_data_field(serde)]
73    pub underlying_index: Option<String>,
74    /// Mark price.
75    #[custom_data_field(serde)]
76    pub mark_price: Option<Decimal>,
77    /// Mid price.
78    #[custom_data_field(serde)]
79    pub mid_price: Option<Decimal>,
80    /// Best bid price.
81    #[custom_data_field(serde)]
82    pub bid_price: Option<Decimal>,
83    /// Best ask price.
84    #[custom_data_field(serde)]
85    pub ask_price: Option<Decimal>,
86    /// Last traded price.
87    #[custom_data_field(serde)]
88    pub last_price: Option<Decimal>,
89    /// Mark implied volatility.
90    #[custom_data_field(serde)]
91    pub mark_iv: Option<Decimal>,
92    /// Bid implied volatility.
93    #[custom_data_field(serde)]
94    pub bid_iv: Option<Decimal>,
95    /// Ask implied volatility.
96    #[custom_data_field(serde)]
97    pub ask_iv: Option<Decimal>,
98    /// Interest rate used in IV calculations.
99    #[custom_data_field(serde)]
100    pub interest_rate: Option<Decimal>,
101    /// Open interest.
102    #[custom_data_field(serde)]
103    pub open_interest: Option<Decimal>,
104    /// Open interest value when provided.
105    #[custom_data_field(serde)]
106    pub open_interest_value: Option<Decimal>,
107    /// 24h volume.
108    #[custom_data_field(serde)]
109    pub volume: Option<Decimal>,
110    /// 24h volume in USD.
111    #[custom_data_field(serde)]
112    pub volume_usd: Option<Decimal>,
113    /// 24h notional volume.
114    #[custom_data_field(serde)]
115    pub volume_notional: Option<Decimal>,
116    /// 24h volume in BTC when provided.
117    #[custom_data_field(serde)]
118    pub volume_btc: Option<Decimal>,
119    /// 24h high.
120    #[custom_data_field(serde)]
121    pub high: Option<Decimal>,
122    /// 24h low.
123    #[custom_data_field(serde)]
124    pub low: Option<Decimal>,
125    /// 24h price change.
126    #[custom_data_field(serde)]
127    pub price_change: Option<Decimal>,
128    /// Estimated delivery price.
129    #[custom_data_field(serde)]
130    pub estimated_delivery_price: Option<Decimal>,
131    /// Settlement/delivery price when present.
132    #[custom_data_field(serde)]
133    pub delivery_price: Option<Decimal>,
134    /// Base currency.
135    #[custom_data_field(serde)]
136    pub base_currency: Option<String>,
137    /// Quote currency.
138    #[custom_data_field(serde)]
139    pub quote_currency: Option<String>,
140    /// Instrument creation time (milliseconds since UNIX epoch).
141    pub creation_timestamp: i64,
142    /// UNIX timestamp (nanoseconds) when the snapshot was observed.
143    pub ts_event: UnixNanos,
144    /// UNIX timestamp (nanoseconds) when the instance was initialized.
145    pub ts_init: UnixNanos,
146}
147
148impl DeribitBookSummary {
149    /// Builds a domain book summary from a venue wire DTO.
150    #[must_use]
151    pub fn from_raw(raw: DeribitBookSummaryRaw, ts: UnixNanos) -> Self {
152        let instrument_id = InstrumentId::new(Symbol::new(&raw.instrument_name), *DERIBIT_VENUE);
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: ts,
182            ts_init: ts,
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, ts);
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;
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(), &DataType::UInt64);
276        assert_eq!(schema.field(3).name(), "ts_init");
277        assert_eq!(schema.field(3).data_type(), &DataType::UInt64);
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}