deribit_http/model/
book.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 15/9/25
5******************************************************************************/
6use pretty_simple_display::{DebugPretty, DisplaySimple};
7use serde::{Deserialize, Serialize};
8use serde_with::skip_serializing_none;
9
10/// Book summary information for an instrument
11#[skip_serializing_none]
12#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
13pub struct BookSummary {
14    /// Instrument name
15    pub instrument_name: String,
16    /// Base currency
17    pub base_currency: String,
18    /// Quote currency (usually USD)
19    pub quote_currency: String,
20    /// 24h trading volume
21    pub volume: f64,
22    /// 24h trading volume in USD
23    pub volume_usd: f64,
24    /// Open interest
25    pub open_interest: f64,
26    /// 24h price change percentage
27    pub price_change: Option<f64>,
28    /// Current mark price
29    pub mark_price: f64,
30    /// Mark implied volatility (options only)
31    pub mark_iv: Option<f64>,
32    /// Best bid price
33    pub bid_price: Option<f64>,
34    /// Best ask price
35    pub ask_price: Option<f64>,
36    /// Mid price (bid + ask) / 2
37    pub mid_price: Option<f64>,
38    /// Last trade price
39    pub last: Option<f64>,
40    /// 24h high price
41    pub high: Option<f64>,
42    /// 24h low price
43    pub low: Option<f64>,
44    /// Estimated delivery price
45    pub estimated_delivery_price: Option<f64>,
46    /// Current funding rate (perpetuals only)
47    pub current_funding: Option<f64>,
48    /// 8h funding rate (perpetuals only)
49    pub funding_8h: Option<f64>,
50    /// Creation timestamp (milliseconds since Unix epoch)
51    pub creation_timestamp: i64,
52    /// Underlying index name
53    pub underlying_index: Option<String>,
54    /// Underlying price
55    pub underlying_price: Option<f64>,
56    /// Interest rate
57    pub interest_rate: Option<f64>,
58}
59
60impl BookSummary {
61    /// Create a new book summary
62    pub fn new(
63        instrument_name: String,
64        base_currency: String,
65        quote_currency: String,
66        mark_price: f64,
67        creation_timestamp: i64,
68    ) -> Self {
69        Self {
70            instrument_name,
71            base_currency,
72            quote_currency,
73            volume: 0.0,
74            volume_usd: 0.0,
75            open_interest: 0.0,
76            price_change: None,
77            mark_price,
78            mark_iv: None,
79            bid_price: None,
80            ask_price: None,
81            mid_price: None,
82            last: None,
83            high: None,
84            low: None,
85            estimated_delivery_price: None,
86            current_funding: None,
87            funding_8h: None,
88            creation_timestamp,
89            // initialize merged optional fields
90            underlying_index: None,
91            underlying_price: None,
92            interest_rate: None,
93        }
94    }
95
96    /// Set volume information
97    pub fn with_volume(mut self, volume: f64, volume_usd: f64) -> Self {
98        self.volume = volume;
99        self.volume_usd = volume_usd;
100        self
101    }
102
103    /// Set price information
104    pub fn with_prices(
105        mut self,
106        bid: Option<f64>,
107        ask: Option<f64>,
108        last: Option<f64>,
109        high: Option<f64>,
110        low: Option<f64>,
111    ) -> Self {
112        self.bid_price = bid;
113        self.ask_price = ask;
114        self.last = last;
115        self.high = high;
116        self.low = low;
117
118        // Calculate mid price if both bid and ask are available
119        if let (Some(bid), Some(ask)) = (bid, ask) {
120            self.mid_price = Some((bid + ask) / 2.0);
121        }
122
123        self
124    }
125
126    /// Set open interest
127    pub fn with_open_interest(mut self, open_interest: f64) -> Self {
128        self.open_interest = open_interest;
129        self
130    }
131
132    /// Set price change percentage
133    pub fn with_price_change(mut self, price_change: f64) -> Self {
134        self.price_change = Some(price_change);
135        self
136    }
137
138    /// Set implied volatility (for options)
139    pub fn with_iv(mut self, mark_iv: f64) -> Self {
140        self.mark_iv = Some(mark_iv);
141        self
142    }
143
144    /// Set funding rates (for perpetuals)
145    pub fn with_funding(mut self, current: f64, funding_8h: f64) -> Self {
146        self.current_funding = Some(current);
147        self.funding_8h = Some(funding_8h);
148        self
149    }
150
151    /// Set estimated delivery price
152    pub fn with_delivery_price(mut self, price: f64) -> Self {
153        self.estimated_delivery_price = Some(price);
154        self
155    }
156
157    /// Get spread (ask - bid)
158    pub fn spread(&self) -> Option<f64> {
159        match (self.bid_price, self.ask_price) {
160            (Some(bid), Some(ask)) => Some(ask - bid),
161            _ => None,
162        }
163    }
164
165    /// Get spread percentage
166    pub fn spread_percentage(&self) -> Option<f64> {
167        match (self.spread(), self.mid_price) {
168            (Some(spread), Some(mid)) if mid > 0.0 => Some((spread / mid) * 100.0),
169            _ => None,
170        }
171    }
172
173    /// Check if this is a perpetual contract
174    pub fn is_perpetual(&self) -> bool {
175        self.instrument_name.contains("PERPETUAL")
176    }
177
178    /// Check if this is an option
179    pub fn is_option(&self) -> bool {
180        // Options end with -C or -P (call/put) but not PERPETUAL
181        !self.is_perpetual()
182            && (self.instrument_name.ends_with("-C") || self.instrument_name.ends_with("-P"))
183    }
184
185    /// Check if this is a future
186    pub fn is_future(&self) -> bool {
187        !self.is_perpetual() && !self.is_option()
188    }
189
190    /// Get 24h price change in absolute terms
191    pub fn price_change_absolute(&self) -> Option<f64> {
192        self.price_change.map(|change| {
193            if let Some(last) = self.last {
194                last * (change / 100.0)
195            } else {
196                self.mark_price * (change / 100.0)
197            }
198        })
199    }
200}
201
202/// Collection of book summaries
203#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
204pub struct BookSummaries {
205    /// List of book summaries
206    pub summaries: Vec<BookSummary>,
207}
208
209impl BookSummaries {
210    /// Create a new collection
211    pub fn new() -> Self {
212        Self {
213            summaries: Vec::new(),
214        }
215    }
216
217    /// Add a book summary
218    pub fn add(&mut self, summary: BookSummary) {
219        self.summaries.push(summary);
220    }
221
222    /// Get summaries by currency
223    pub fn by_currency(&self, currency: String) -> Vec<&BookSummary> {
224        self.summaries
225            .iter()
226            .filter(|s| s.base_currency == currency)
227            .collect()
228    }
229
230    /// Get summaries by instrument type
231    pub fn perpetuals(&self) -> Vec<&BookSummary> {
232        self.summaries.iter().filter(|s| s.is_perpetual()).collect()
233    }
234
235    /// Get option summaries
236    pub fn options(&self) -> Vec<&BookSummary> {
237        self.summaries.iter().filter(|s| s.is_option()).collect()
238    }
239
240    /// Get future summaries
241    pub fn futures(&self) -> Vec<&BookSummary> {
242        self.summaries.iter().filter(|s| s.is_future()).collect()
243    }
244
245    /// Sort by volume (descending)
246    pub fn sort_by_volume(&mut self) {
247        self.summaries
248            .sort_by(|a, b| b.volume_usd.partial_cmp(&a.volume_usd).unwrap());
249    }
250
251    /// Sort by open interest (descending)
252    pub fn sort_by_open_interest(&mut self) {
253        self.summaries
254            .sort_by(|a, b| b.open_interest.partial_cmp(&a.open_interest).unwrap());
255    }
256}
257
258impl Default for BookSummaries {
259    fn default() -> Self {
260        Self::new()
261    }
262}
263
264/// Order book entry
265#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
266pub struct OrderBookEntry {
267    /// Price level
268    pub price: f64,
269    /// Amount at this price level
270    pub amount: f64,
271}
272
273impl OrderBookEntry {
274    /// Create a new order book entry
275    pub fn new(price: f64, amount: f64) -> Self {
276        Self { price, amount }
277    }
278
279    /// Calculate notional value
280    pub fn notional(&self) -> f64 {
281        self.price * self.amount
282    }
283}
284
285/// Order book data
286#[skip_serializing_none]
287#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
288pub struct OrderBook {
289    /// Instrument name
290    pub instrument_name: String,
291    /// Timestamp of the order book
292    pub timestamp: i64,
293    /// Bid levels (sorted by price descending)
294    pub bids: Vec<OrderBookEntry>,
295    /// Ask levels (sorted by price ascending)
296    pub asks: Vec<OrderBookEntry>,
297    /// Change ID for incremental updates
298    pub change_id: u64,
299    /// Previous change ID
300    pub prev_change_id: Option<u64>,
301}
302
303impl OrderBook {
304    /// Create a new empty order book
305    pub fn new(instrument_name: String, timestamp: i64, change_id: u64) -> Self {
306        Self {
307            instrument_name,
308            timestamp,
309            bids: Vec::new(),
310            asks: Vec::new(),
311            change_id,
312            prev_change_id: None,
313        }
314    }
315
316    /// Get best bid price
317    pub fn best_bid(&self) -> Option<f64> {
318        self.bids.first().map(|entry| entry.price)
319    }
320
321    /// Get best ask price
322    pub fn best_ask(&self) -> Option<f64> {
323        self.asks.first().map(|entry| entry.price)
324    }
325
326    /// Get bid-ask spread
327    pub fn spread(&self) -> Option<f64> {
328        match (self.best_ask(), self.best_bid()) {
329            (Some(ask), Some(bid)) => Some(ask - bid),
330            _ => None,
331        }
332    }
333
334    /// Get mid price
335    pub fn mid_price(&self) -> Option<f64> {
336        match (self.best_ask(), self.best_bid()) {
337            (Some(ask), Some(bid)) => Some((ask + bid) / 2.0),
338            _ => None,
339        }
340    }
341
342    /// Calculate total bid volume
343    pub fn total_bid_volume(&self) -> f64 {
344        self.bids.iter().map(|entry| entry.amount).sum()
345    }
346
347    /// Calculate total ask volume
348    pub fn total_ask_volume(&self) -> f64 {
349        self.asks.iter().map(|entry| entry.amount).sum()
350    }
351
352    /// Get volume at specific price level
353    pub fn volume_at_price(&self, price: f64, is_bid: bool) -> f64 {
354        let levels = if is_bid { &self.bids } else { &self.asks };
355        levels
356            .iter()
357            .find(|entry| (entry.price - price).abs() < f64::EPSILON)
358            .map(|entry| entry.amount)
359            .unwrap_or(0.0)
360    }
361}