Skip to main content

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/// Mark price history data point
278///
279/// Represents a single mark price value at a specific timestamp,
280/// returned by `/public/get_mark_price_history`.
281#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
282pub struct MarkPricePoint {
283    /// Timestamp in milliseconds since Unix epoch
284    pub timestamp: i64,
285    /// Mark price value
286    pub mark_price: f64,
287}
288
289impl MarkPricePoint {
290    /// Create a new mark price point
291    #[must_use]
292    pub fn new(timestamp: i64, mark_price: f64) -> Self {
293        Self {
294            timestamp,
295            mark_price,
296        }
297    }
298
299    /// Create from raw API response tuple [timestamp, mark_price]
300    #[must_use]
301    pub fn from_tuple(data: (i64, f64)) -> Self {
302        Self {
303            timestamp: data.0,
304            mark_price: data.1,
305        }
306    }
307}
308
309/// Mark price history collection
310///
311/// Collection of mark price history points for an instrument,
312/// returned by `/public/get_mark_price_history`.
313#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
314pub struct MarkPriceHistory {
315    /// Instrument name
316    pub instrument_name: String,
317    /// Collection of mark price points
318    pub points: Vec<MarkPricePoint>,
319}
320
321impl MarkPriceHistory {
322    /// Create a new empty mark price history
323    #[must_use]
324    pub fn new(instrument_name: String) -> Self {
325        Self {
326            instrument_name,
327            points: Vec::new(),
328        }
329    }
330
331    /// Create from raw API response data
332    #[must_use]
333    pub fn from_raw(instrument_name: String, data: Vec<(i64, f64)>) -> Self {
334        Self {
335            instrument_name,
336            points: data.into_iter().map(MarkPricePoint::from_tuple).collect(),
337        }
338    }
339
340    /// Add a mark price point
341    pub fn add_point(&mut self, point: MarkPricePoint) {
342        self.points.push(point);
343    }
344
345    /// Get the latest mark price point
346    #[must_use]
347    pub fn latest(&self) -> Option<&MarkPricePoint> {
348        self.points.iter().max_by_key(|p| p.timestamp)
349    }
350
351    /// Get the earliest mark price point
352    #[must_use]
353    pub fn earliest(&self) -> Option<&MarkPricePoint> {
354        self.points.iter().min_by_key(|p| p.timestamp)
355    }
356
357    /// Get the number of points
358    #[must_use]
359    pub fn len(&self) -> usize {
360        self.points.len()
361    }
362
363    /// Check if the history is empty
364    #[must_use]
365    pub fn is_empty(&self) -> bool {
366        self.points.is_empty()
367    }
368}
369
370/// Trade volume data for a currency
371///
372/// Aggregated 24h trade volumes for different instrument types,
373/// returned by `/public/get_trade_volumes`.
374#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
375pub struct TradeVolume {
376    /// Currency code (e.g., "BTC", "ETH")
377    pub currency: String,
378    /// 24h trade volume for put options
379    pub puts_volume: f64,
380    /// 24h trade volume for call options
381    pub calls_volume: f64,
382    /// 24h trade volume for futures
383    pub futures_volume: f64,
384    /// 24h trade volume for spot
385    #[serde(default)]
386    pub spot_volume: f64,
387    /// 7-day trade volume for put options (extended)
388    #[serde(skip_serializing_if = "Option::is_none")]
389    pub puts_volume_7d: Option<f64>,
390    /// 30-day trade volume for put options (extended)
391    #[serde(skip_serializing_if = "Option::is_none")]
392    pub puts_volume_30d: Option<f64>,
393    /// 7-day trade volume for call options (extended)
394    #[serde(skip_serializing_if = "Option::is_none")]
395    pub calls_volume_7d: Option<f64>,
396    /// 30-day trade volume for call options (extended)
397    #[serde(skip_serializing_if = "Option::is_none")]
398    pub calls_volume_30d: Option<f64>,
399    /// 7-day trade volume for futures (extended)
400    #[serde(skip_serializing_if = "Option::is_none")]
401    pub futures_volume_7d: Option<f64>,
402    /// 30-day trade volume for futures (extended)
403    #[serde(skip_serializing_if = "Option::is_none")]
404    pub futures_volume_30d: Option<f64>,
405    /// 7-day trade volume for spot (extended)
406    #[serde(skip_serializing_if = "Option::is_none")]
407    pub spot_volume_7d: Option<f64>,
408    /// 30-day trade volume for spot (extended)
409    #[serde(skip_serializing_if = "Option::is_none")]
410    pub spot_volume_30d: Option<f64>,
411}
412
413impl TradeVolume {
414    /// Create a new trade volume with basic 24h data
415    #[must_use]
416    pub fn new(
417        currency: String,
418        puts_volume: f64,
419        calls_volume: f64,
420        futures_volume: f64,
421        spot_volume: f64,
422    ) -> Self {
423        Self {
424            currency,
425            puts_volume,
426            calls_volume,
427            futures_volume,
428            spot_volume,
429            puts_volume_7d: None,
430            puts_volume_30d: None,
431            calls_volume_7d: None,
432            calls_volume_30d: None,
433            futures_volume_7d: None,
434            futures_volume_30d: None,
435            spot_volume_7d: None,
436            spot_volume_30d: None,
437        }
438    }
439
440    /// Calculate total options volume (puts + calls)
441    #[must_use]
442    pub fn total_options_volume(&self) -> f64 {
443        self.puts_volume + self.calls_volume
444    }
445
446    /// Calculate total 24h volume across all instrument types
447    #[must_use]
448    pub fn total_volume(&self) -> f64 {
449        self.puts_volume + self.calls_volume + self.futures_volume + self.spot_volume
450    }
451
452    /// Calculate put/call ratio
453    #[must_use]
454    pub fn put_call_ratio(&self) -> Option<f64> {
455        if self.calls_volume > 0.0 {
456            Some(self.puts_volume / self.calls_volume)
457        } else {
458            None
459        }
460    }
461}
462
463/// Volatility index OHLC candle
464///
465/// Represents a single volatility index candle with OHLC data,
466/// returned by `/public/get_volatility_index_data`.
467#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
468pub struct VolatilityIndexCandle {
469    /// Timestamp in milliseconds since Unix epoch
470    pub timestamp: i64,
471    /// Open volatility value
472    pub open: f64,
473    /// High volatility value
474    pub high: f64,
475    /// Low volatility value
476    pub low: f64,
477    /// Close volatility value
478    pub close: f64,
479}
480
481impl VolatilityIndexCandle {
482    /// Create a new volatility index candle
483    #[must_use]
484    pub fn new(timestamp: i64, open: f64, high: f64, low: f64, close: f64) -> Self {
485        Self {
486            timestamp,
487            open,
488            high,
489            low,
490            close,
491        }
492    }
493
494    /// Create from raw API response tuple [timestamp, open, high, low, close]
495    #[must_use]
496    pub fn from_tuple(data: (i64, f64, f64, f64, f64)) -> Self {
497        Self {
498            timestamp: data.0,
499            open: data.1,
500            high: data.2,
501            low: data.3,
502            close: data.4,
503        }
504    }
505
506    /// Calculate the range (high - low)
507    #[must_use]
508    pub fn range(&self) -> f64 {
509        self.high - self.low
510    }
511
512    /// Check if volatility increased (close > open)
513    #[must_use]
514    pub fn is_increasing(&self) -> bool {
515        self.close > self.open
516    }
517
518    /// Check if volatility decreased (close < open)
519    #[must_use]
520    pub fn is_decreasing(&self) -> bool {
521        self.close < self.open
522    }
523}
524
525/// Volatility index data response
526///
527/// Collection of volatility index candles with optional continuation token,
528/// returned by `/public/get_volatility_index_data`.
529#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
530pub struct VolatilityIndexData {
531    /// Currency for this volatility index
532    pub currency: String,
533    /// Collection of volatility candles
534    pub data: Vec<VolatilityIndexCandle>,
535    /// Continuation token for pagination
536    #[serde(skip_serializing_if = "Option::is_none")]
537    pub continuation: Option<String>,
538}
539
540impl VolatilityIndexData {
541    /// Create a new empty volatility index data
542    #[must_use]
543    pub fn new(currency: String) -> Self {
544        Self {
545            currency,
546            data: Vec::new(),
547            continuation: None,
548        }
549    }
550
551    /// Create from raw API response data
552    #[must_use]
553    pub fn from_raw(
554        currency: String,
555        data: Vec<(i64, f64, f64, f64, f64)>,
556        continuation: Option<String>,
557    ) -> Self {
558        Self {
559            currency,
560            data: data
561                .into_iter()
562                .map(VolatilityIndexCandle::from_tuple)
563                .collect(),
564            continuation,
565        }
566    }
567
568    /// Get the latest candle
569    #[must_use]
570    pub fn latest(&self) -> Option<&VolatilityIndexCandle> {
571        self.data.iter().max_by_key(|c| c.timestamp)
572    }
573
574    /// Get the earliest candle
575    #[must_use]
576    pub fn earliest(&self) -> Option<&VolatilityIndexCandle> {
577        self.data.iter().min_by_key(|c| c.timestamp)
578    }
579
580    /// Check if there are more results available
581    #[must_use]
582    pub fn has_more(&self) -> bool {
583        self.continuation.is_some()
584    }
585
586    /// Get the number of candles
587    #[must_use]
588    pub fn len(&self) -> usize {
589        self.data.len()
590    }
591
592    /// Check if the data is empty
593    #[must_use]
594    pub fn is_empty(&self) -> bool {
595        self.data.is_empty()
596    }
597}
598
599/// Index type filter for supported index names
600///
601/// Used to filter index names by type in `/public/get_supported_index_names`.
602#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
603#[serde(rename_all = "lowercase")]
604pub enum IndexType {
605    /// All index types
606    #[default]
607    All,
608    /// Spot price indexes
609    Spot,
610    /// Derivative price indexes
611    Derivative,
612}
613
614impl IndexType {
615    /// Get the string representation for API requests
616    #[must_use]
617    pub fn as_str(&self) -> &'static str {
618        match self {
619            Self::All => "all",
620            Self::Spot => "spot",
621            Self::Derivative => "derivative",
622        }
623    }
624}
625
626impl std::fmt::Display for IndexType {
627    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
628        write!(f, "{}", self.as_str())
629    }
630}
631
632#[cfg(test)]
633mod tests {
634    use super::*;
635
636    fn create_test_ticker() -> Ticker {
637        Ticker {
638            instrument_name: "BTC-PERPETUAL".to_string(),
639            timestamp: 1640995200000,
640            best_bid_price: Some(50000.0),
641            best_bid_amount: Some(1.5),
642            best_ask_price: Some(50100.0),
643            best_ask_amount: Some(2.0),
644            last_price: Some(50050.0),
645            mark_price: Some(50025.0),
646            index_price: Some(50000.0),
647            open_interest: 1000.0,
648            volume_24h: 500.0,
649            volume_usd_24h: 25000000.0,
650            price_change_24h: 2.5,
651            high_24h: Some(51000.0),
652            low_24h: Some(49000.0),
653            underlying_price: Some(50000.0),
654            underlying_index: Some("btc_usd".to_string()),
655            instrument_kind: Some(InstrumentKind::Future),
656            current_funding: Some(0.0001),
657            funding_8h: Some(0.0008),
658            iv: None,
659            greeks: None,
660            interest_rate: Some(0.05),
661        }
662    }
663
664    #[test]
665    fn test_ticker_spread() {
666        let ticker = create_test_ticker();
667        assert_eq!(ticker.spread(), Some(100.0)); // 50100 - 50000
668
669        let mut ticker_no_bid = ticker.clone();
670        ticker_no_bid.best_bid_price = None;
671        assert_eq!(ticker_no_bid.spread(), None);
672    }
673
674    #[test]
675    fn test_ticker_mid_price() {
676        let ticker = create_test_ticker();
677        assert_eq!(ticker.mid_price(), Some(50050.0)); // (50100 + 50000) / 2
678
679        let mut ticker_no_ask = ticker.clone();
680        ticker_no_ask.best_ask_price = None;
681        assert_eq!(ticker_no_ask.mid_price(), None);
682    }
683
684    #[test]
685    fn test_ticker_spread_percentage() {
686        let ticker = create_test_ticker();
687        let expected = (100.0 / 50050.0) * 100.0;
688        assert!((ticker.spread_percentage().unwrap() - expected).abs() < 0.001);
689
690        let mut ticker_no_spread = ticker.clone();
691        ticker_no_spread.best_bid_price = None;
692        assert_eq!(ticker_no_spread.spread_percentage(), None);
693    }
694
695    #[test]
696    fn test_ticker_has_valid_spread() {
697        let ticker = create_test_ticker();
698        assert!(ticker.has_valid_spread());
699
700        let mut ticker_no_bid = ticker.clone();
701        ticker_no_bid.best_bid_price = None;
702        assert!(!ticker_no_bid.has_valid_spread());
703    }
704
705    #[test]
706    fn test_order_book_entry_new() {
707        let entry = OrderBookEntry::new(50000.0, 1.5);
708        assert_eq!(entry.price, 50000.0);
709        assert_eq!(entry.amount, 1.5);
710    }
711
712    #[test]
713    fn test_order_book_entry_notional() {
714        let entry = OrderBookEntry::new(50000.0, 1.5);
715        assert_eq!(entry.notional(), 75000.0);
716    }
717
718    #[test]
719    fn test_order_book_new() {
720        let book = OrderBook::new("BTC-PERPETUAL".to_string(), 1640995200000, 12345);
721        assert_eq!(book.instrument_name, "BTC-PERPETUAL");
722        assert_eq!(book.timestamp, 1640995200000);
723        assert_eq!(book.change_id, 12345);
724        assert!(book.bids.is_empty());
725        assert!(book.asks.is_empty());
726        assert_eq!(book.prev_change_id, None);
727    }
728
729    #[test]
730    fn test_order_book_best_prices() {
731        let mut book = OrderBook::new("BTC-PERPETUAL".to_string(), 1640995200000, 12345);
732        book.bids.push(OrderBookEntry::new(50000.0, 1.0));
733        book.bids.push(OrderBookEntry::new(49900.0, 2.0));
734        book.asks.push(OrderBookEntry::new(50100.0, 1.5));
735        book.asks.push(OrderBookEntry::new(50200.0, 2.5));
736
737        assert_eq!(book.best_bid(), Some(50000.0));
738        assert_eq!(book.best_ask(), Some(50100.0));
739    }
740
741    #[test]
742    fn test_order_book_spread() {
743        let mut book = OrderBook::new("BTC-PERPETUAL".to_string(), 1640995200000, 12345);
744        book.bids.push(OrderBookEntry::new(50000.0, 1.0));
745        book.asks.push(OrderBookEntry::new(50100.0, 1.5));
746
747        assert_eq!(book.spread(), Some(100.0));
748    }
749
750    #[test]
751    fn test_order_book_mid_price() {
752        let mut book = OrderBook::new("BTC-PERPETUAL".to_string(), 1640995200000, 12345);
753        book.bids.push(OrderBookEntry::new(50000.0, 1.0));
754        book.asks.push(OrderBookEntry::new(50100.0, 1.5));
755
756        assert_eq!(book.mid_price(), Some(50050.0));
757    }
758
759    #[test]
760    fn test_order_book_total_volumes() {
761        let mut book = OrderBook::new("BTC-PERPETUAL".to_string(), 1640995200000, 12345);
762        book.bids.push(OrderBookEntry::new(50000.0, 1.0));
763        book.bids.push(OrderBookEntry::new(49900.0, 2.0));
764        book.asks.push(OrderBookEntry::new(50100.0, 1.5));
765        book.asks.push(OrderBookEntry::new(50200.0, 2.5));
766
767        assert_eq!(book.total_bid_volume(), 3.0);
768        assert_eq!(book.total_ask_volume(), 4.0);
769    }
770
771    #[test]
772    fn test_order_book_volume_at_price() {
773        let mut book = OrderBook::new("BTC-PERPETUAL".to_string(), 1640995200000, 12345);
774        book.bids.push(OrderBookEntry::new(50000.0, 1.0));
775        book.asks.push(OrderBookEntry::new(50100.0, 1.5));
776
777        assert_eq!(book.volume_at_price(50000.0, true), 1.0);
778        assert_eq!(book.volume_at_price(50100.0, false), 1.5);
779        assert_eq!(book.volume_at_price(49000.0, true), 0.0);
780    }
781
782    #[test]
783    fn test_candle_is_bullish() {
784        let bullish_candle = Candle {
785            timestamp: 1640995200000,
786            open: 50000.0,
787            high: 51000.0,
788            low: 49500.0,
789            close: 50500.0,
790            volume: 100.0,
791            trades: Some(50),
792        };
793        assert!(bullish_candle.is_bullish());
794        assert!(!bullish_candle.is_bearish());
795    }
796
797    #[test]
798    fn test_candle_is_bearish() {
799        let bearish_candle = Candle {
800            timestamp: 1640995200000,
801            open: 50000.0,
802            high: 50200.0,
803            low: 49000.0,
804            close: 49500.0,
805            volume: 100.0,
806            trades: Some(50),
807        };
808        assert!(bearish_candle.is_bearish());
809        assert!(!bearish_candle.is_bullish());
810    }
811
812    #[test]
813    fn test_candle_body_size() {
814        let candle = Candle {
815            timestamp: 1640995200000,
816            open: 50000.0,
817            high: 51000.0,
818            low: 49000.0,
819            close: 50500.0,
820            volume: 100.0,
821            trades: Some(50),
822        };
823        assert_eq!(candle.body_size(), 500.0);
824    }
825
826    #[test]
827    fn test_candle_upper_shadow() {
828        let candle = Candle {
829            timestamp: 1640995200000,
830            open: 50000.0,
831            high: 51000.0,
832            low: 49000.0,
833            close: 50500.0,
834            volume: 100.0,
835            trades: Some(50),
836        };
837        assert_eq!(candle.upper_shadow(), 500.0); // 51000 - 50500
838    }
839
840    #[test]
841    fn test_candle_lower_shadow() {
842        let candle = Candle {
843            timestamp: 1640995200000,
844            open: 50000.0,
845            high: 51000.0,
846            low: 49000.0,
847            close: 50500.0,
848            volume: 100.0,
849            trades: Some(50),
850        };
851        assert_eq!(candle.lower_shadow(), 1000.0); // 50000 - 49000
852    }
853
854    #[test]
855    fn test_candle_range() {
856        let candle = Candle {
857            timestamp: 1640995200000,
858            open: 50000.0,
859            high: 51000.0,
860            low: 49000.0,
861            close: 50500.0,
862            volume: 100.0,
863            trades: Some(50),
864        };
865        assert_eq!(candle.range(), 2000.0); // 51000 - 49000
866    }
867
868    #[test]
869    fn test_greeks_creation() {
870        let greeks = Greeks {
871            delta: 0.5,
872            gamma: 0.01,
873            theta: -0.05,
874            vega: 0.1,
875            rho: Some(0.02),
876        };
877        assert_eq!(greeks.delta, 0.5);
878        assert_eq!(greeks.rho, Some(0.02));
879    }
880
881    #[test]
882    fn test_market_stats_creation() {
883        let stats = MarketStats {
884            currency: "BTC".to_string(),
885            volume_24h: 1000.0,
886            volume_change_24h: 5.0,
887            price_change_24h: 2.5,
888            high_24h: 51000.0,
889            low_24h: 49000.0,
890            active_instruments: 50,
891            total_open_interest: 10000.0,
892        };
893        assert_eq!(stats.currency, "BTC");
894        assert_eq!(stats.active_instruments, 50);
895    }
896
897    #[test]
898    fn test_serialization() {
899        let ticker = create_test_ticker();
900        let json = serde_json::to_string(&ticker).unwrap();
901        let deserialized: Ticker = serde_json::from_str(&json).unwrap();
902        assert_eq!(ticker.instrument_name, deserialized.instrument_name);
903        assert_eq!(ticker.best_bid_price, deserialized.best_bid_price);
904    }
905
906    #[test]
907    fn test_debug_and_display_implementations() {
908        let ticker = create_test_ticker();
909        let debug_str = format!("{:?}", ticker);
910        let display_str = format!("{}", ticker);
911
912        assert!(debug_str.contains("BTC-PERPETUAL"));
913        assert!(display_str.contains("BTC-PERPETUAL"));
914    }
915
916    #[test]
917    fn test_mark_price_point_new() {
918        let point = MarkPricePoint::new(1640995200000, 50000.0);
919        assert_eq!(point.timestamp, 1640995200000);
920        assert!((point.mark_price - 50000.0).abs() < f64::EPSILON);
921    }
922
923    #[test]
924    fn test_mark_price_point_from_tuple() {
925        let point = MarkPricePoint::from_tuple((1640995200000, 50000.0));
926        assert_eq!(point.timestamp, 1640995200000);
927        assert!((point.mark_price - 50000.0).abs() < f64::EPSILON);
928    }
929
930    #[test]
931    fn test_mark_price_history_new() {
932        let history = MarkPriceHistory::new("BTC-25JUN21-50000-C".to_string());
933        assert_eq!(history.instrument_name, "BTC-25JUN21-50000-C");
934        assert!(history.is_empty());
935        assert_eq!(history.len(), 0);
936    }
937
938    #[test]
939    fn test_mark_price_history_from_raw() {
940        let data = vec![
941            (1640995200000, 0.5165),
942            (1640995201000, 0.5166),
943            (1640995202000, 0.5167),
944        ];
945        let history = MarkPriceHistory::from_raw("BTC-25JUN21-50000-C".to_string(), data);
946        assert_eq!(history.len(), 3);
947        assert!(!history.is_empty());
948    }
949
950    #[test]
951    fn test_mark_price_history_latest_earliest() {
952        let data = vec![
953            (1640995200000, 0.5165),
954            (1640995202000, 0.5167),
955            (1640995201000, 0.5166),
956        ];
957        let history = MarkPriceHistory::from_raw("BTC-25JUN21-50000-C".to_string(), data);
958
959        let latest = history.latest();
960        assert!(latest.is_some());
961        assert_eq!(latest.map(|p| p.timestamp), Some(1640995202000));
962
963        let earliest = history.earliest();
964        assert!(earliest.is_some());
965        assert_eq!(earliest.map(|p| p.timestamp), Some(1640995200000));
966    }
967
968    #[test]
969    fn test_mark_price_history_serialization() {
970        let history = MarkPriceHistory::from_raw(
971            "BTC-25JUN21-50000-C".to_string(),
972            vec![(1640995200000, 0.5165)],
973        );
974        let json = serde_json::to_string(&history).unwrap();
975        let deserialized: MarkPriceHistory = serde_json::from_str(&json).unwrap();
976        assert_eq!(history, deserialized);
977    }
978
979    #[test]
980    fn test_trade_volume_new() {
981        let volume = TradeVolume::new("BTC".to_string(), 48.0, 145.0, 6.25, 11.1);
982        assert_eq!(volume.currency, "BTC");
983        assert!((volume.puts_volume - 48.0).abs() < f64::EPSILON);
984        assert!((volume.calls_volume - 145.0).abs() < f64::EPSILON);
985        assert!((volume.futures_volume - 6.25).abs() < f64::EPSILON);
986        assert!((volume.spot_volume - 11.1).abs() < f64::EPSILON);
987    }
988
989    #[test]
990    fn test_trade_volume_total_options() {
991        let volume = TradeVolume::new("BTC".to_string(), 48.0, 145.0, 6.25, 11.1);
992        assert!((volume.total_options_volume() - 193.0).abs() < f64::EPSILON);
993    }
994
995    #[test]
996    fn test_trade_volume_total() {
997        let volume = TradeVolume::new("BTC".to_string(), 48.0, 145.0, 6.25, 11.1);
998        let expected = 48.0 + 145.0 + 6.25 + 11.1;
999        assert!((volume.total_volume() - expected).abs() < f64::EPSILON);
1000    }
1001
1002    #[test]
1003    fn test_trade_volume_put_call_ratio() {
1004        let volume = TradeVolume::new("BTC".to_string(), 48.0, 145.0, 6.25, 11.1);
1005        let ratio = volume.put_call_ratio();
1006        assert!(ratio.is_some());
1007        assert!((ratio.unwrap() - (48.0 / 145.0)).abs() < 0.001);
1008
1009        let volume_zero_calls = TradeVolume::new("BTC".to_string(), 48.0, 0.0, 6.25, 11.1);
1010        assert!(volume_zero_calls.put_call_ratio().is_none());
1011    }
1012
1013    #[test]
1014    fn test_trade_volume_serialization() {
1015        let volume = TradeVolume::new("BTC".to_string(), 48.0, 145.0, 6.25, 11.1);
1016        let json = serde_json::to_string(&volume).unwrap();
1017        let deserialized: TradeVolume = serde_json::from_str(&json).unwrap();
1018        assert_eq!(volume.currency, deserialized.currency);
1019        assert!((volume.puts_volume - deserialized.puts_volume).abs() < f64::EPSILON);
1020    }
1021
1022    #[test]
1023    fn test_volatility_index_candle_new() {
1024        let candle = VolatilityIndexCandle::new(1640995200000, 0.21, 0.22, 0.20, 0.215);
1025        assert_eq!(candle.timestamp, 1640995200000);
1026        assert!((candle.open - 0.21).abs() < f64::EPSILON);
1027        assert!((candle.high - 0.22).abs() < f64::EPSILON);
1028        assert!((candle.low - 0.20).abs() < f64::EPSILON);
1029        assert!((candle.close - 0.215).abs() < f64::EPSILON);
1030    }
1031
1032    #[test]
1033    fn test_volatility_index_candle_from_tuple() {
1034        let candle = VolatilityIndexCandle::from_tuple((1640995200000, 0.21, 0.22, 0.20, 0.215));
1035        assert_eq!(candle.timestamp, 1640995200000);
1036        assert!((candle.range() - 0.02).abs() < f64::EPSILON);
1037    }
1038
1039    #[test]
1040    fn test_volatility_index_candle_increasing_decreasing() {
1041        let increasing = VolatilityIndexCandle::new(1640995200000, 0.20, 0.22, 0.19, 0.21);
1042        assert!(increasing.is_increasing());
1043        assert!(!increasing.is_decreasing());
1044
1045        let decreasing = VolatilityIndexCandle::new(1640995200000, 0.21, 0.22, 0.19, 0.20);
1046        assert!(decreasing.is_decreasing());
1047        assert!(!decreasing.is_increasing());
1048    }
1049
1050    #[test]
1051    fn test_volatility_index_data_new() {
1052        let data = VolatilityIndexData::new("BTC".to_string());
1053        assert_eq!(data.currency, "BTC");
1054        assert!(data.is_empty());
1055        assert_eq!(data.len(), 0);
1056        assert!(!data.has_more());
1057    }
1058
1059    #[test]
1060    fn test_volatility_index_data_from_raw() {
1061        let raw_data = vec![
1062            (1640995200000, 0.21, 0.22, 0.20, 0.215),
1063            (1640995260000, 0.215, 0.23, 0.21, 0.22),
1064        ];
1065        let data = VolatilityIndexData::from_raw("BTC".to_string(), raw_data, None);
1066        assert_eq!(data.len(), 2);
1067        assert!(!data.has_more());
1068    }
1069
1070    #[test]
1071    fn test_volatility_index_data_with_continuation() {
1072        let raw_data = vec![(1640995200000, 0.21, 0.22, 0.20, 0.215)];
1073        let data = VolatilityIndexData::from_raw(
1074            "BTC".to_string(),
1075            raw_data,
1076            Some("next_page_token".to_string()),
1077        );
1078        assert!(data.has_more());
1079        assert_eq!(data.continuation, Some("next_page_token".to_string()));
1080    }
1081
1082    #[test]
1083    fn test_volatility_index_data_latest_earliest() {
1084        let raw_data = vec![
1085            (1640995200000, 0.21, 0.22, 0.20, 0.215),
1086            (1640995320000, 0.22, 0.24, 0.21, 0.23),
1087            (1640995260000, 0.215, 0.23, 0.21, 0.22),
1088        ];
1089        let data = VolatilityIndexData::from_raw("BTC".to_string(), raw_data, None);
1090
1091        let latest = data.latest();
1092        assert!(latest.is_some());
1093        assert_eq!(latest.map(|c| c.timestamp), Some(1640995320000));
1094
1095        let earliest = data.earliest();
1096        assert!(earliest.is_some());
1097        assert_eq!(earliest.map(|c| c.timestamp), Some(1640995200000));
1098    }
1099
1100    #[test]
1101    fn test_volatility_index_data_serialization() {
1102        let data = VolatilityIndexData::from_raw(
1103            "BTC".to_string(),
1104            vec![(1640995200000, 0.21, 0.22, 0.20, 0.215)],
1105            None,
1106        );
1107        let json = serde_json::to_string(&data).unwrap();
1108        let deserialized: VolatilityIndexData = serde_json::from_str(&json).unwrap();
1109        assert_eq!(data, deserialized);
1110    }
1111
1112    #[test]
1113    fn test_index_type_default() {
1114        let index_type = IndexType::default();
1115        assert_eq!(index_type, IndexType::All);
1116    }
1117
1118    #[test]
1119    fn test_index_type_as_str() {
1120        assert_eq!(IndexType::All.as_str(), "all");
1121        assert_eq!(IndexType::Spot.as_str(), "spot");
1122        assert_eq!(IndexType::Derivative.as_str(), "derivative");
1123    }
1124
1125    #[test]
1126    fn test_index_type_display() {
1127        assert_eq!(format!("{}", IndexType::All), "all");
1128        assert_eq!(format!("{}", IndexType::Spot), "spot");
1129        assert_eq!(format!("{}", IndexType::Derivative), "derivative");
1130    }
1131
1132    #[test]
1133    fn test_index_type_serialization() {
1134        let index_type = IndexType::Spot;
1135        let json = serde_json::to_string(&index_type).unwrap();
1136        assert_eq!(json, "\"spot\"");
1137
1138        let deserialized: IndexType = serde_json::from_str(&json).unwrap();
1139        assert_eq!(deserialized, IndexType::Spot);
1140    }
1141}