1use crate::model::instrument::InstrumentKind;
7use pretty_simple_display::{DebugPretty, DisplaySimple};
8use serde::{Deserialize, Serialize};
9
10#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
12pub struct Ticker {
13 pub instrument_name: String,
15 pub timestamp: i64,
17 pub best_bid_price: Option<f64>,
19 pub best_bid_amount: Option<f64>,
21 pub best_ask_price: Option<f64>,
23 pub best_ask_amount: Option<f64>,
25 pub last_price: Option<f64>,
27 pub mark_price: Option<f64>,
29 pub index_price: Option<f64>,
31 pub open_interest: f64,
33 pub volume_24h: f64,
35 pub volume_usd_24h: f64,
37 pub price_change_24h: f64,
39 pub high_24h: Option<f64>,
41 pub low_24h: Option<f64>,
43 pub underlying_price: Option<f64>,
45 pub underlying_index: Option<String>,
47 pub instrument_kind: Option<InstrumentKind>,
49 pub current_funding: Option<f64>,
51 pub funding_8h: Option<f64>,
53 pub iv: Option<f64>,
55 pub greeks: Option<Greeks>,
57 pub interest_rate: Option<f64>,
59}
60
61impl Ticker {
62 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 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 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 pub fn has_valid_spread(&self) -> bool {
88 self.best_bid_price.is_some() && self.best_ask_price.is_some()
89 }
90}
91
92#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
94pub struct OrderBookEntry {
95 pub price: f64,
97 pub amount: f64,
99}
100
101impl OrderBookEntry {
102 pub fn new(price: f64, amount: f64) -> Self {
104 Self { price, amount }
105 }
106
107 pub fn notional(&self) -> f64 {
109 self.price * self.amount
110 }
111}
112
113#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
115pub struct OrderBook {
116 pub instrument_name: String,
118 pub timestamp: i64,
120 pub bids: Vec<OrderBookEntry>,
122 pub asks: Vec<OrderBookEntry>,
124 pub change_id: u64,
126 pub prev_change_id: Option<u64>,
128}
129
130impl OrderBook {
131 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 pub fn best_bid(&self) -> Option<f64> {
145 self.bids.first().map(|entry| entry.price)
146 }
147
148 pub fn best_ask(&self) -> Option<f64> {
150 self.asks.first().map(|entry| entry.price)
151 }
152
153 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 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 pub fn total_bid_volume(&self) -> f64 {
171 self.bids.iter().map(|entry| entry.amount).sum()
172 }
173
174 pub fn total_ask_volume(&self) -> f64 {
176 self.asks.iter().map(|entry| entry.amount).sum()
177 }
178
179 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#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
192pub struct Greeks {
193 pub delta: f64,
195 pub gamma: f64,
197 pub theta: f64,
199 pub vega: f64,
201 pub rho: Option<f64>,
203}
204
205#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
207pub struct MarketStats {
208 pub currency: String,
210 pub volume_24h: f64,
212 pub volume_change_24h: f64,
214 pub price_change_24h: f64,
216 pub high_24h: f64,
218 pub low_24h: f64,
220 pub active_instruments: u32,
222 pub total_open_interest: f64,
224}
225
226#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
228pub struct Candle {
229 pub timestamp: i64,
231 pub open: f64,
233 pub high: f64,
235 pub low: f64,
237 pub close: f64,
239 pub volume: f64,
241 pub trades: Option<u64>,
243}
244
245impl Candle {
246 pub fn is_bullish(&self) -> bool {
248 self.close > self.open
249 }
250
251 pub fn is_bearish(&self) -> bool {
253 self.close < self.open
254 }
255
256 pub fn body_size(&self) -> f64 {
258 (self.close - self.open).abs()
259 }
260
261 pub fn upper_shadow(&self) -> f64 {
263 self.high - self.close.max(self.open)
264 }
265
266 pub fn lower_shadow(&self) -> f64 {
268 self.close.min(self.open) - self.low
269 }
270
271 pub fn range(&self) -> f64 {
273 self.high - self.low
274 }
275}
276
277#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
282pub struct MarkPricePoint {
283 pub timestamp: i64,
285 pub mark_price: f64,
287}
288
289impl MarkPricePoint {
290 #[must_use]
292 pub fn new(timestamp: i64, mark_price: f64) -> Self {
293 Self {
294 timestamp,
295 mark_price,
296 }
297 }
298
299 #[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#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
314pub struct MarkPriceHistory {
315 pub instrument_name: String,
317 pub points: Vec<MarkPricePoint>,
319}
320
321impl MarkPriceHistory {
322 #[must_use]
324 pub fn new(instrument_name: String) -> Self {
325 Self {
326 instrument_name,
327 points: Vec::new(),
328 }
329 }
330
331 #[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 pub fn add_point(&mut self, point: MarkPricePoint) {
342 self.points.push(point);
343 }
344
345 #[must_use]
347 pub fn latest(&self) -> Option<&MarkPricePoint> {
348 self.points.iter().max_by_key(|p| p.timestamp)
349 }
350
351 #[must_use]
353 pub fn earliest(&self) -> Option<&MarkPricePoint> {
354 self.points.iter().min_by_key(|p| p.timestamp)
355 }
356
357 #[must_use]
359 pub fn len(&self) -> usize {
360 self.points.len()
361 }
362
363 #[must_use]
365 pub fn is_empty(&self) -> bool {
366 self.points.is_empty()
367 }
368}
369
370#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
375pub struct TradeVolume {
376 pub currency: String,
378 pub puts_volume: f64,
380 pub calls_volume: f64,
382 pub futures_volume: f64,
384 #[serde(default)]
386 pub spot_volume: f64,
387 #[serde(skip_serializing_if = "Option::is_none")]
389 pub puts_volume_7d: Option<f64>,
390 #[serde(skip_serializing_if = "Option::is_none")]
392 pub puts_volume_30d: Option<f64>,
393 #[serde(skip_serializing_if = "Option::is_none")]
395 pub calls_volume_7d: Option<f64>,
396 #[serde(skip_serializing_if = "Option::is_none")]
398 pub calls_volume_30d: Option<f64>,
399 #[serde(skip_serializing_if = "Option::is_none")]
401 pub futures_volume_7d: Option<f64>,
402 #[serde(skip_serializing_if = "Option::is_none")]
404 pub futures_volume_30d: Option<f64>,
405 #[serde(skip_serializing_if = "Option::is_none")]
407 pub spot_volume_7d: Option<f64>,
408 #[serde(skip_serializing_if = "Option::is_none")]
410 pub spot_volume_30d: Option<f64>,
411}
412
413impl TradeVolume {
414 #[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 #[must_use]
442 pub fn total_options_volume(&self) -> f64 {
443 self.puts_volume + self.calls_volume
444 }
445
446 #[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 #[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#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
468pub struct VolatilityIndexCandle {
469 pub timestamp: i64,
471 pub open: f64,
473 pub high: f64,
475 pub low: f64,
477 pub close: f64,
479}
480
481impl VolatilityIndexCandle {
482 #[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 #[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 #[must_use]
508 pub fn range(&self) -> f64 {
509 self.high - self.low
510 }
511
512 #[must_use]
514 pub fn is_increasing(&self) -> bool {
515 self.close > self.open
516 }
517
518 #[must_use]
520 pub fn is_decreasing(&self) -> bool {
521 self.close < self.open
522 }
523}
524
525#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
530pub struct VolatilityIndexData {
531 pub currency: String,
533 pub data: Vec<VolatilityIndexCandle>,
535 #[serde(skip_serializing_if = "Option::is_none")]
537 pub continuation: Option<String>,
538}
539
540impl VolatilityIndexData {
541 #[must_use]
543 pub fn new(currency: String) -> Self {
544 Self {
545 currency,
546 data: Vec::new(),
547 continuation: None,
548 }
549 }
550
551 #[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 #[must_use]
570 pub fn latest(&self) -> Option<&VolatilityIndexCandle> {
571 self.data.iter().max_by_key(|c| c.timestamp)
572 }
573
574 #[must_use]
576 pub fn earliest(&self) -> Option<&VolatilityIndexCandle> {
577 self.data.iter().min_by_key(|c| c.timestamp)
578 }
579
580 #[must_use]
582 pub fn has_more(&self) -> bool {
583 self.continuation.is_some()
584 }
585
586 #[must_use]
588 pub fn len(&self) -> usize {
589 self.data.len()
590 }
591
592 #[must_use]
594 pub fn is_empty(&self) -> bool {
595 self.data.is_empty()
596 }
597}
598
599#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
603#[serde(rename_all = "lowercase")]
604pub enum IndexType {
605 #[default]
607 All,
608 Spot,
610 Derivative,
612}
613
614impl IndexType {
615 #[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)); 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)); 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); }
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); }
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); }
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}