deribit-base 0.3.1

Base library with common structs, traits, and logic for Deribit API clients
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
/******************************************************************************
   Author: Joaquín Béjar García
   Email: jb@taunais.com
   Date: 21/7/25
******************************************************************************/
use pretty_simple_display::{DebugPretty, DisplaySimple};
use serde::{Deserialize, Serialize};

/// Book summary information for an instrument
#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
pub struct BookSummary {
    /// Instrument name
    pub instrument_name: String,
    /// Base currency
    pub base_currency: String,
    /// Quote currency (usually USD)
    pub quote_currency: String,
    /// 24h trading volume
    pub volume: f64,
    /// 24h trading volume in USD
    pub volume_usd: f64,
    /// Open interest
    pub open_interest: f64,
    /// 24h price change percentage
    #[serde(skip_serializing_if = "Option::is_none")]
    pub price_change: Option<f64>,
    /// Current mark price
    pub mark_price: f64,
    /// Mark implied volatility (options only)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub mark_iv: Option<f64>,
    /// Best bid price
    #[serde(skip_serializing_if = "Option::is_none")]
    pub bid_price: Option<f64>,
    /// Best ask price
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ask_price: Option<f64>,
    /// Mid price (bid + ask) / 2
    #[serde(skip_serializing_if = "Option::is_none")]
    pub mid_price: Option<f64>,
    /// Last trade price
    #[serde(skip_serializing_if = "Option::is_none")]
    pub last: Option<f64>,
    /// 24h high price
    #[serde(skip_serializing_if = "Option::is_none")]
    pub high: Option<f64>,
    /// 24h low price
    #[serde(skip_serializing_if = "Option::is_none")]
    pub low: Option<f64>,
    /// Estimated delivery price
    #[serde(skip_serializing_if = "Option::is_none")]
    pub estimated_delivery_price: Option<f64>,
    /// Current funding rate (perpetuals only)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub current_funding: Option<f64>,
    /// 8h funding rate (perpetuals only)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub funding_8h: Option<f64>,
    /// Creation timestamp (milliseconds since Unix epoch)
    pub creation_timestamp: i64,
    // Additional optional fields merged from deribit-http types.rs
    /// Underlying index name
    #[serde(skip_serializing_if = "Option::is_none")]
    pub underlying_index: Option<String>,
    /// Underlying price
    #[serde(skip_serializing_if = "Option::is_none")]
    pub underlying_price: Option<f64>,
    /// Interest rate
    #[serde(skip_serializing_if = "Option::is_none")]
    pub interest_rate: Option<f64>,
}

impl BookSummary {
    /// Create a new book summary
    pub fn new(
        instrument_name: String,
        base_currency: String,
        quote_currency: String,
        mark_price: f64,
        creation_timestamp: i64,
    ) -> Self {
        Self {
            instrument_name,
            base_currency,
            quote_currency,
            volume: 0.0,
            volume_usd: 0.0,
            open_interest: 0.0,
            price_change: None,
            mark_price,
            mark_iv: None,
            bid_price: None,
            ask_price: None,
            mid_price: None,
            last: None,
            high: None,
            low: None,
            estimated_delivery_price: None,
            current_funding: None,
            funding_8h: None,
            creation_timestamp,
            // initialize merged optional fields
            underlying_index: None,
            underlying_price: None,
            interest_rate: None,
        }
    }

    /// Set volume information
    pub fn with_volume(mut self, volume: f64, volume_usd: f64) -> Self {
        self.volume = volume;
        self.volume_usd = volume_usd;
        self
    }

    /// Set price information
    pub fn with_prices(
        mut self,
        bid: Option<f64>,
        ask: Option<f64>,
        last: Option<f64>,
        high: Option<f64>,
        low: Option<f64>,
    ) -> Self {
        self.bid_price = bid;
        self.ask_price = ask;
        self.last = last;
        self.high = high;
        self.low = low;

        // Calculate mid price if both bid and ask are available
        if let (Some(bid), Some(ask)) = (bid, ask) {
            self.mid_price = Some((bid + ask) / 2.0);
        }

        self
    }

    /// Set open interest
    pub fn with_open_interest(mut self, open_interest: f64) -> Self {
        self.open_interest = open_interest;
        self
    }

    /// Set price change percentage
    pub fn with_price_change(mut self, price_change: f64) -> Self {
        self.price_change = Some(price_change);
        self
    }

    /// Set implied volatility (for options)
    pub fn with_iv(mut self, mark_iv: f64) -> Self {
        self.mark_iv = Some(mark_iv);
        self
    }

    /// Set funding rates (for perpetuals)
    pub fn with_funding(mut self, current: f64, funding_8h: f64) -> Self {
        self.current_funding = Some(current);
        self.funding_8h = Some(funding_8h);
        self
    }

    /// Set estimated delivery price
    pub fn with_delivery_price(mut self, price: f64) -> Self {
        self.estimated_delivery_price = Some(price);
        self
    }

    /// Get spread (ask - bid)
    pub fn spread(&self) -> Option<f64> {
        match (self.bid_price, self.ask_price) {
            (Some(bid), Some(ask)) => Some(ask - bid),
            _ => None,
        }
    }

    /// Get spread percentage
    pub fn spread_percentage(&self) -> Option<f64> {
        match (self.spread(), self.mid_price) {
            (Some(spread), Some(mid)) if mid > 0.0 => Some((spread / mid) * 100.0),
            _ => None,
        }
    }

    /// Check if this is a perpetual contract
    pub fn is_perpetual(&self) -> bool {
        self.instrument_name.contains("PERPETUAL")
    }

    /// Check if this is an option
    pub fn is_option(&self) -> bool {
        // Options end with -C or -P (call/put) but not PERPETUAL
        !self.is_perpetual()
            && (self.instrument_name.ends_with("-C") || self.instrument_name.ends_with("-P"))
    }

    /// Check if this is a future
    pub fn is_future(&self) -> bool {
        !self.is_perpetual() && !self.is_option()
    }

    /// Get 24h price change in absolute terms
    pub fn price_change_absolute(&self) -> Option<f64> {
        self.price_change.map(|change| {
            if let Some(last) = self.last {
                last * (change / 100.0)
            } else {
                self.mark_price * (change / 100.0)
            }
        })
    }
}

/// Collection of book summaries
#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
pub struct BookSummaries {
    /// List of book summaries
    pub summaries: Vec<BookSummary>,
}

impl BookSummaries {
    /// Create a new collection
    pub fn new() -> Self {
        Self {
            summaries: Vec::new(),
        }
    }

    /// Add a book summary
    pub fn add(&mut self, summary: BookSummary) {
        self.summaries.push(summary);
    }

    /// Get summaries by currency
    pub fn by_currency(&self, currency: String) -> Vec<&BookSummary> {
        self.summaries
            .iter()
            .filter(|s| s.base_currency == currency)
            .collect()
    }

    /// Get summaries by instrument type
    pub fn perpetuals(&self) -> Vec<&BookSummary> {
        self.summaries.iter().filter(|s| s.is_perpetual()).collect()
    }

    /// Get option summaries
    pub fn options(&self) -> Vec<&BookSummary> {
        self.summaries.iter().filter(|s| s.is_option()).collect()
    }

    /// Get future summaries
    pub fn futures(&self) -> Vec<&BookSummary> {
        self.summaries.iter().filter(|s| s.is_future()).collect()
    }

    /// Sort by volume (descending)
    pub fn sort_by_volume(&mut self) {
        self.summaries
            .sort_by(|a, b| b.volume_usd.partial_cmp(&a.volume_usd).unwrap());
    }

    /// Sort by open interest (descending)
    pub fn sort_by_open_interest(&mut self) {
        self.summaries
            .sort_by(|a, b| b.open_interest.partial_cmp(&a.open_interest).unwrap());
    }
}

impl Default for BookSummaries {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_book_summary_creation() {
        let summary = BookSummary::new(
            "BTC-PERPETUAL".to_string(),
            "BTC".to_string(),
            "USD".to_string(),
            45000.0,
            1640995200000,
        );

        assert_eq!(summary.instrument_name, "BTC-PERPETUAL");
        assert_eq!(summary.base_currency, "BTC".to_string());
        assert_eq!(summary.mark_price, 45000.0);
        assert!(summary.is_perpetual());
    }

    #[test]
    fn test_book_summary_builder() {
        let summary = BookSummary::new(
            "BTC-25MAR23-50000-C".to_string(),
            "BTC".to_string(),
            "USD".to_string(),
            2500.0,
            1640995200000,
        )
        .with_volume(100.0, 4500000.0)
        .with_prices(
            Some(2480.0),
            Some(2520.0),
            Some(2500.0),
            Some(2600.0),
            Some(2400.0),
        )
        .with_iv(85.5);

        assert_eq!(summary.volume, 100.0);
        assert_eq!(summary.volume_usd, 4500000.0);
        assert_eq!(summary.bid_price, Some(2480.0));
        assert_eq!(summary.ask_price, Some(2520.0));
        assert_eq!(summary.mid_price, Some(2500.0));
        assert_eq!(summary.mark_iv, Some(85.5));
        assert!(summary.is_option());
    }

    #[test]
    fn test_spread_calculation() {
        let summary = BookSummary::new(
            "BTC-PERPETUAL".to_string(),
            "BTC".to_string(),
            "USD".to_string(),
            45000.0,
            1640995200000,
        )
        .with_prices(Some(44950.0), Some(45050.0), None, None, None);

        assert_eq!(summary.spread(), Some(100.0));
        assert_eq!(summary.mid_price, Some(45000.0));

        let spread_pct = summary.spread_percentage().unwrap();
        assert!((spread_pct - 0.2222).abs() < 0.001); // ~0.22%
    }

    #[test]
    fn test_instrument_type_detection() {
        let perpetual = BookSummary::new(
            "BTC-PERPETUAL".to_string(),
            "BTC".to_string(),
            "USD".to_string(),
            45000.0,
            0,
        );
        assert!(perpetual.is_perpetual());
        assert!(!perpetual.is_option());
        assert!(!perpetual.is_future());

        let option = BookSummary::new(
            "BTC-25MAR23-50000-C".to_string(),
            "BTC".to_string(),
            "USD".to_string(),
            2500.0,
            0,
        );
        assert!(!option.is_perpetual());
        assert!(option.is_option());
        assert!(!option.is_future());

        let future = BookSummary::new(
            "BTC-25MAR23".to_string(),
            "BTC".to_string(),
            "USD".to_string(),
            45000.0,
            0,
        );
        assert!(!future.is_perpetual());
        assert!(!future.is_option());
        assert!(future.is_future());
    }

    #[test]
    fn test_book_summaries_collection() {
        let mut summaries = BookSummaries::new();

        summaries.add(
            BookSummary::new(
                "BTC-PERPETUAL".to_string(),
                "BTC".to_string(),
                "USD".to_string(),
                45000.0,
                0,
            )
            .with_volume(1000.0, 45000000.0),
        );

        summaries.add(
            BookSummary::new(
                "ETH-PERPETUAL".to_string(),
                "ETH".to_string(),
                "USD".to_string(),
                3000.0,
                0,
            )
            .with_volume(500.0, 1500000.0),
        );

        assert_eq!(summaries.summaries.len(), 2);
        assert_eq!(summaries.by_currency("BTC".to_string()).len(), 1);
        assert_eq!(summaries.perpetuals().len(), 2);

        summaries.sort_by_volume();
        assert_eq!(summaries.summaries[0].base_currency, "BTC".to_string());
    }

    #[test]
    fn test_serde() {
        let summary = BookSummary::new(
            "BTC-PERPETUAL".to_string(),
            "BTC".to_string(),
            "USD".to_string(),
            45000.0,
            1640995200000,
        )
        .with_funding(0.0001, 0.0008);

        let json = serde_json::to_string(&summary).unwrap();
        let deserialized: BookSummary = serde_json::from_str(&json).unwrap();
        assert_eq!(summary, deserialized);
    }
}