deribit_base/model/
market_data.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 21/7/25
5******************************************************************************/
6use crate::model::instrument::InstrumentKind;
7use pretty_simple_display::{DebugPretty, DisplaySimple};
8use serde::{Deserialize, Serialize};
9
10/// Ticker information
11#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
12pub struct Ticker {
13    /// Instrument name
14    pub instrument_name: String,
15    /// Timestamp of the ticker data
16    pub timestamp: i64,
17    /// Best bid price
18    pub best_bid_price: Option<f64>,
19    /// Best bid amount
20    pub best_bid_amount: Option<f64>,
21    /// Best ask price
22    pub best_ask_price: Option<f64>,
23    /// Best ask amount
24    pub best_ask_amount: Option<f64>,
25    /// Last trade price
26    pub last_price: Option<f64>,
27    /// Mark price
28    pub mark_price: Option<f64>,
29    /// Index price
30    pub index_price: Option<f64>,
31    /// Open interest
32    pub open_interest: f64,
33    /// 24h volume
34    pub volume_24h: f64,
35    /// 24h volume in USD
36    pub volume_usd_24h: f64,
37    /// 24h price change
38    pub price_change_24h: f64,
39    /// High price in 24h
40    pub high_24h: Option<f64>,
41    /// Low price in 24h
42    pub low_24h: Option<f64>,
43    /// Underlying price (for derivatives)
44    pub underlying_price: Option<f64>,
45    /// Underlying index
46    pub underlying_index: Option<String>,
47    /// Instrument kind
48    pub instrument_kind: Option<InstrumentKind>,
49    /// Current funding rate (for perpetuals)
50    pub current_funding: Option<f64>,
51    /// Funding 8h rate
52    pub funding_8h: Option<f64>,
53    /// Implied volatility (for options)
54    pub iv: Option<f64>,
55    /// Greeks (for options)
56    pub greeks: Option<Greeks>,
57    /// Interest rate
58    pub interest_rate: Option<f64>,
59}
60
61impl Ticker {
62    /// Calculate bid-ask spread
63    pub fn spread(&self) -> Option<f64> {
64        match (self.best_ask_price, self.best_bid_price) {
65            (Some(ask), Some(bid)) => Some(ask - bid),
66            _ => None,
67        }
68    }
69
70    /// Calculate mid price
71    pub fn mid_price(&self) -> Option<f64> {
72        match (self.best_ask_price, self.best_bid_price) {
73            (Some(ask), Some(bid)) => Some((ask + bid) / 2.0),
74            _ => None,
75        }
76    }
77
78    /// Calculate spread percentage
79    pub fn spread_percentage(&self) -> Option<f64> {
80        match (self.spread(), self.mid_price()) {
81            (Some(spread), Some(mid)) if mid != 0.0 => Some((spread / mid) * 100.0),
82            _ => None,
83        }
84    }
85
86    /// Check if there's a valid bid-ask spread
87    pub fn has_valid_spread(&self) -> bool {
88        self.best_bid_price.is_some() && self.best_ask_price.is_some()
89    }
90}
91
92/// Order book entry
93#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
94pub struct OrderBookEntry {
95    /// Price level
96    pub price: f64,
97    /// Amount at this price level
98    pub amount: f64,
99}
100
101impl OrderBookEntry {
102    /// Create a new order book entry
103    pub fn new(price: f64, amount: f64) -> Self {
104        Self { price, amount }
105    }
106
107    /// Calculate notional value
108    pub fn notional(&self) -> f64 {
109        self.price * self.amount
110    }
111}
112
113/// Order book data
114#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
115pub struct OrderBook {
116    /// Instrument name
117    pub instrument_name: String,
118    /// Timestamp of the order book
119    pub timestamp: i64,
120    /// Bid levels (sorted by price descending)
121    pub bids: Vec<OrderBookEntry>,
122    /// Ask levels (sorted by price ascending)
123    pub asks: Vec<OrderBookEntry>,
124    /// Change ID for incremental updates
125    pub change_id: u64,
126    /// Previous change ID
127    pub prev_change_id: Option<u64>,
128}
129
130impl OrderBook {
131    /// Create a new empty order book
132    pub fn new(instrument_name: String, timestamp: i64, change_id: u64) -> Self {
133        Self {
134            instrument_name,
135            timestamp,
136            bids: Vec::new(),
137            asks: Vec::new(),
138            change_id,
139            prev_change_id: None,
140        }
141    }
142
143    /// Get best bid price
144    pub fn best_bid(&self) -> Option<f64> {
145        self.bids.first().map(|entry| entry.price)
146    }
147
148    /// Get best ask price
149    pub fn best_ask(&self) -> Option<f64> {
150        self.asks.first().map(|entry| entry.price)
151    }
152
153    /// Get bid-ask spread
154    pub fn spread(&self) -> Option<f64> {
155        match (self.best_ask(), self.best_bid()) {
156            (Some(ask), Some(bid)) => Some(ask - bid),
157            _ => None,
158        }
159    }
160
161    /// Get mid price
162    pub fn mid_price(&self) -> Option<f64> {
163        match (self.best_ask(), self.best_bid()) {
164            (Some(ask), Some(bid)) => Some((ask + bid) / 2.0),
165            _ => None,
166        }
167    }
168
169    /// Calculate total bid volume
170    pub fn total_bid_volume(&self) -> f64 {
171        self.bids.iter().map(|entry| entry.amount).sum()
172    }
173
174    /// Calculate total ask volume
175    pub fn total_ask_volume(&self) -> f64 {
176        self.asks.iter().map(|entry| entry.amount).sum()
177    }
178
179    /// Get volume at specific price level
180    pub fn volume_at_price(&self, price: f64, is_bid: bool) -> f64 {
181        let levels = if is_bid { &self.bids } else { &self.asks };
182        levels
183            .iter()
184            .find(|entry| (entry.price - price).abs() < f64::EPSILON)
185            .map(|entry| entry.amount)
186            .unwrap_or(0.0)
187    }
188}
189
190/// Greeks for options
191#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
192pub struct Greeks {
193    /// Delta - sensitivity to underlying price changes
194    pub delta: f64,
195    /// Gamma - rate of change of delta
196    pub gamma: f64,
197    /// Theta - time decay
198    pub theta: f64,
199    /// Vega - sensitivity to volatility changes
200    pub vega: f64,
201    /// Rho - sensitivity to interest rate changes
202    pub rho: Option<f64>,
203}
204
205/// Market statistics
206#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
207pub struct MarketStats {
208    /// Currency
209    pub currency: String,
210    /// Total volume in 24h
211    pub volume_24h: f64,
212    /// Volume change in 24h
213    pub volume_change_24h: f64,
214    /// Price change in 24h
215    pub price_change_24h: f64,
216    /// High price in 24h
217    pub high_24h: f64,
218    /// Low price in 24h
219    pub low_24h: f64,
220    /// Number of active instruments
221    pub active_instruments: u32,
222    /// Total open interest
223    pub total_open_interest: f64,
224}
225
226/// Candlestick/OHLCV data
227#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
228pub struct Candle {
229    /// Timestamp
230    pub timestamp: i64,
231    /// Open price
232    pub open: f64,
233    /// High price
234    pub high: f64,
235    /// Low price
236    pub low: f64,
237    /// Close price
238    pub close: f64,
239    /// Volume
240    pub volume: f64,
241    /// Number of trades
242    pub trades: Option<u64>,
243}
244
245impl Candle {
246    /// Check if this is a bullish candle
247    pub fn is_bullish(&self) -> bool {
248        self.close > self.open
249    }
250
251    /// Check if this is a bearish candle
252    pub fn is_bearish(&self) -> bool {
253        self.close < self.open
254    }
255
256    /// Calculate the body size
257    pub fn body_size(&self) -> f64 {
258        (self.close - self.open).abs()
259    }
260
261    /// Calculate the upper shadow
262    pub fn upper_shadow(&self) -> f64 {
263        self.high - self.close.max(self.open)
264    }
265
266    /// Calculate the lower shadow
267    pub fn lower_shadow(&self) -> f64 {
268        self.close.min(self.open) - self.low
269    }
270
271    /// Calculate the range (high - low)
272    pub fn range(&self) -> f64 {
273        self.high - self.low
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280
281    fn create_test_ticker() -> Ticker {
282        Ticker {
283            instrument_name: "BTC-PERPETUAL".to_string(),
284            timestamp: 1640995200000,
285            best_bid_price: Some(50000.0),
286            best_bid_amount: Some(1.5),
287            best_ask_price: Some(50100.0),
288            best_ask_amount: Some(2.0),
289            last_price: Some(50050.0),
290            mark_price: Some(50025.0),
291            index_price: Some(50000.0),
292            open_interest: 1000.0,
293            volume_24h: 500.0,
294            volume_usd_24h: 25000000.0,
295            price_change_24h: 2.5,
296            high_24h: Some(51000.0),
297            low_24h: Some(49000.0),
298            underlying_price: Some(50000.0),
299            underlying_index: Some("btc_usd".to_string()),
300            instrument_kind: Some(InstrumentKind::Future),
301            current_funding: Some(0.0001),
302            funding_8h: Some(0.0008),
303            iv: None,
304            greeks: None,
305            interest_rate: Some(0.05),
306        }
307    }
308
309    #[test]
310    fn test_ticker_spread() {
311        let ticker = create_test_ticker();
312        assert_eq!(ticker.spread(), Some(100.0)); // 50100 - 50000
313
314        let mut ticker_no_bid = ticker.clone();
315        ticker_no_bid.best_bid_price = None;
316        assert_eq!(ticker_no_bid.spread(), None);
317    }
318
319    #[test]
320    fn test_ticker_mid_price() {
321        let ticker = create_test_ticker();
322        assert_eq!(ticker.mid_price(), Some(50050.0)); // (50100 + 50000) / 2
323
324        let mut ticker_no_ask = ticker.clone();
325        ticker_no_ask.best_ask_price = None;
326        assert_eq!(ticker_no_ask.mid_price(), None);
327    }
328
329    #[test]
330    fn test_ticker_spread_percentage() {
331        let ticker = create_test_ticker();
332        let expected = (100.0 / 50050.0) * 100.0;
333        assert!((ticker.spread_percentage().unwrap() - expected).abs() < 0.001);
334
335        let mut ticker_no_spread = ticker.clone();
336        ticker_no_spread.best_bid_price = None;
337        assert_eq!(ticker_no_spread.spread_percentage(), None);
338    }
339
340    #[test]
341    fn test_ticker_has_valid_spread() {
342        let ticker = create_test_ticker();
343        assert!(ticker.has_valid_spread());
344
345        let mut ticker_no_bid = ticker.clone();
346        ticker_no_bid.best_bid_price = None;
347        assert!(!ticker_no_bid.has_valid_spread());
348    }
349
350    #[test]
351    fn test_order_book_entry_new() {
352        let entry = OrderBookEntry::new(50000.0, 1.5);
353        assert_eq!(entry.price, 50000.0);
354        assert_eq!(entry.amount, 1.5);
355    }
356
357    #[test]
358    fn test_order_book_entry_notional() {
359        let entry = OrderBookEntry::new(50000.0, 1.5);
360        assert_eq!(entry.notional(), 75000.0);
361    }
362
363    #[test]
364    fn test_order_book_new() {
365        let book = OrderBook::new("BTC-PERPETUAL".to_string(), 1640995200000, 12345);
366        assert_eq!(book.instrument_name, "BTC-PERPETUAL");
367        assert_eq!(book.timestamp, 1640995200000);
368        assert_eq!(book.change_id, 12345);
369        assert!(book.bids.is_empty());
370        assert!(book.asks.is_empty());
371        assert_eq!(book.prev_change_id, None);
372    }
373
374    #[test]
375    fn test_order_book_best_prices() {
376        let mut book = OrderBook::new("BTC-PERPETUAL".to_string(), 1640995200000, 12345);
377        book.bids.push(OrderBookEntry::new(50000.0, 1.0));
378        book.bids.push(OrderBookEntry::new(49900.0, 2.0));
379        book.asks.push(OrderBookEntry::new(50100.0, 1.5));
380        book.asks.push(OrderBookEntry::new(50200.0, 2.5));
381
382        assert_eq!(book.best_bid(), Some(50000.0));
383        assert_eq!(book.best_ask(), Some(50100.0));
384    }
385
386    #[test]
387    fn test_order_book_spread() {
388        let mut book = OrderBook::new("BTC-PERPETUAL".to_string(), 1640995200000, 12345);
389        book.bids.push(OrderBookEntry::new(50000.0, 1.0));
390        book.asks.push(OrderBookEntry::new(50100.0, 1.5));
391
392        assert_eq!(book.spread(), Some(100.0));
393    }
394
395    #[test]
396    fn test_order_book_mid_price() {
397        let mut book = OrderBook::new("BTC-PERPETUAL".to_string(), 1640995200000, 12345);
398        book.bids.push(OrderBookEntry::new(50000.0, 1.0));
399        book.asks.push(OrderBookEntry::new(50100.0, 1.5));
400
401        assert_eq!(book.mid_price(), Some(50050.0));
402    }
403
404    #[test]
405    fn test_order_book_total_volumes() {
406        let mut book = OrderBook::new("BTC-PERPETUAL".to_string(), 1640995200000, 12345);
407        book.bids.push(OrderBookEntry::new(50000.0, 1.0));
408        book.bids.push(OrderBookEntry::new(49900.0, 2.0));
409        book.asks.push(OrderBookEntry::new(50100.0, 1.5));
410        book.asks.push(OrderBookEntry::new(50200.0, 2.5));
411
412        assert_eq!(book.total_bid_volume(), 3.0);
413        assert_eq!(book.total_ask_volume(), 4.0);
414    }
415
416    #[test]
417    fn test_order_book_volume_at_price() {
418        let mut book = OrderBook::new("BTC-PERPETUAL".to_string(), 1640995200000, 12345);
419        book.bids.push(OrderBookEntry::new(50000.0, 1.0));
420        book.asks.push(OrderBookEntry::new(50100.0, 1.5));
421
422        assert_eq!(book.volume_at_price(50000.0, true), 1.0);
423        assert_eq!(book.volume_at_price(50100.0, false), 1.5);
424        assert_eq!(book.volume_at_price(49000.0, true), 0.0);
425    }
426
427    #[test]
428    fn test_candle_is_bullish() {
429        let bullish_candle = Candle {
430            timestamp: 1640995200000,
431            open: 50000.0,
432            high: 51000.0,
433            low: 49500.0,
434            close: 50500.0,
435            volume: 100.0,
436            trades: Some(50),
437        };
438        assert!(bullish_candle.is_bullish());
439        assert!(!bullish_candle.is_bearish());
440    }
441
442    #[test]
443    fn test_candle_is_bearish() {
444        let bearish_candle = Candle {
445            timestamp: 1640995200000,
446            open: 50000.0,
447            high: 50200.0,
448            low: 49000.0,
449            close: 49500.0,
450            volume: 100.0,
451            trades: Some(50),
452        };
453        assert!(bearish_candle.is_bearish());
454        assert!(!bearish_candle.is_bullish());
455    }
456
457    #[test]
458    fn test_candle_body_size() {
459        let candle = Candle {
460            timestamp: 1640995200000,
461            open: 50000.0,
462            high: 51000.0,
463            low: 49000.0,
464            close: 50500.0,
465            volume: 100.0,
466            trades: Some(50),
467        };
468        assert_eq!(candle.body_size(), 500.0);
469    }
470
471    #[test]
472    fn test_candle_upper_shadow() {
473        let candle = Candle {
474            timestamp: 1640995200000,
475            open: 50000.0,
476            high: 51000.0,
477            low: 49000.0,
478            close: 50500.0,
479            volume: 100.0,
480            trades: Some(50),
481        };
482        assert_eq!(candle.upper_shadow(), 500.0); // 51000 - 50500
483    }
484
485    #[test]
486    fn test_candle_lower_shadow() {
487        let candle = Candle {
488            timestamp: 1640995200000,
489            open: 50000.0,
490            high: 51000.0,
491            low: 49000.0,
492            close: 50500.0,
493            volume: 100.0,
494            trades: Some(50),
495        };
496        assert_eq!(candle.lower_shadow(), 1000.0); // 50000 - 49000
497    }
498
499    #[test]
500    fn test_candle_range() {
501        let candle = Candle {
502            timestamp: 1640995200000,
503            open: 50000.0,
504            high: 51000.0,
505            low: 49000.0,
506            close: 50500.0,
507            volume: 100.0,
508            trades: Some(50),
509        };
510        assert_eq!(candle.range(), 2000.0); // 51000 - 49000
511    }
512
513    #[test]
514    fn test_greeks_creation() {
515        let greeks = Greeks {
516            delta: 0.5,
517            gamma: 0.01,
518            theta: -0.05,
519            vega: 0.1,
520            rho: Some(0.02),
521        };
522        assert_eq!(greeks.delta, 0.5);
523        assert_eq!(greeks.rho, Some(0.02));
524    }
525
526    #[test]
527    fn test_market_stats_creation() {
528        let stats = MarketStats {
529            currency: "BTC".to_string(),
530            volume_24h: 1000.0,
531            volume_change_24h: 5.0,
532            price_change_24h: 2.5,
533            high_24h: 51000.0,
534            low_24h: 49000.0,
535            active_instruments: 50,
536            total_open_interest: 10000.0,
537        };
538        assert_eq!(stats.currency, "BTC");
539        assert_eq!(stats.active_instruments, 50);
540    }
541
542    #[test]
543    fn test_serialization() {
544        let ticker = create_test_ticker();
545        let json = serde_json::to_string(&ticker).unwrap();
546        let deserialized: Ticker = serde_json::from_str(&json).unwrap();
547        assert_eq!(ticker.instrument_name, deserialized.instrument_name);
548        assert_eq!(ticker.best_bid_price, deserialized.best_bid_price);
549    }
550
551    #[test]
552    fn test_debug_and_display_implementations() {
553        let ticker = create_test_ticker();
554        let debug_str = format!("{:?}", ticker);
555        let display_str = format!("{}", ticker);
556
557        assert!(debug_str.contains("BTC-PERPETUAL"));
558        assert!(display_str.contains("BTC-PERPETUAL"));
559    }
560}