1use crate::{impl_json_debug_pretty, impl_json_display};
8use serde::{Deserialize, Serialize};
9
10#[derive(Clone, PartialEq, Serialize, Deserialize)]
12pub struct BookSummary {
13 pub instrument_name: String,
15 pub base_currency: String,
17 pub quote_currency: String,
19 pub volume: f64,
21 pub volume_usd: f64,
23 pub open_interest: f64,
25 #[serde(skip_serializing_if = "Option::is_none")]
27 pub price_change: Option<f64>,
28 pub mark_price: f64,
30 #[serde(skip_serializing_if = "Option::is_none")]
32 pub mark_iv: Option<f64>,
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub bid_price: Option<f64>,
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub ask_price: Option<f64>,
39 #[serde(skip_serializing_if = "Option::is_none")]
41 pub mid_price: Option<f64>,
42 #[serde(skip_serializing_if = "Option::is_none")]
44 pub last: Option<f64>,
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub high: Option<f64>,
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub low: Option<f64>,
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub estimated_delivery_price: Option<f64>,
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub current_funding: Option<f64>,
57 #[serde(skip_serializing_if = "Option::is_none")]
59 pub funding_8h: Option<f64>,
60 pub creation_timestamp: i64,
62 #[serde(skip_serializing_if = "Option::is_none")]
65 pub underlying_index: Option<String>,
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub underlying_price: Option<f64>,
69 #[serde(skip_serializing_if = "Option::is_none")]
71 pub interest_rate: Option<f64>,
72}
73
74impl BookSummary {
75 pub fn new(
77 instrument_name: String,
78 base_currency: String,
79 quote_currency: String,
80 mark_price: f64,
81 creation_timestamp: i64,
82 ) -> Self {
83 Self {
84 instrument_name,
85 base_currency,
86 quote_currency,
87 volume: 0.0,
88 volume_usd: 0.0,
89 open_interest: 0.0,
90 price_change: None,
91 mark_price,
92 mark_iv: None,
93 bid_price: None,
94 ask_price: None,
95 mid_price: None,
96 last: None,
97 high: None,
98 low: None,
99 estimated_delivery_price: None,
100 current_funding: None,
101 funding_8h: None,
102 creation_timestamp,
103 underlying_index: None,
105 underlying_price: None,
106 interest_rate: None,
107 }
108 }
109
110 pub fn with_volume(mut self, volume: f64, volume_usd: f64) -> Self {
112 self.volume = volume;
113 self.volume_usd = volume_usd;
114 self
115 }
116
117 pub fn with_prices(
119 mut self,
120 bid: Option<f64>,
121 ask: Option<f64>,
122 last: Option<f64>,
123 high: Option<f64>,
124 low: Option<f64>,
125 ) -> Self {
126 self.bid_price = bid;
127 self.ask_price = ask;
128 self.last = last;
129 self.high = high;
130 self.low = low;
131
132 if let (Some(bid), Some(ask)) = (bid, ask) {
134 self.mid_price = Some((bid + ask) / 2.0);
135 }
136
137 self
138 }
139
140 pub fn with_open_interest(mut self, open_interest: f64) -> Self {
142 self.open_interest = open_interest;
143 self
144 }
145
146 pub fn with_price_change(mut self, price_change: f64) -> Self {
148 self.price_change = Some(price_change);
149 self
150 }
151
152 pub fn with_iv(mut self, mark_iv: f64) -> Self {
154 self.mark_iv = Some(mark_iv);
155 self
156 }
157
158 pub fn with_funding(mut self, current: f64, funding_8h: f64) -> Self {
160 self.current_funding = Some(current);
161 self.funding_8h = Some(funding_8h);
162 self
163 }
164
165 pub fn with_delivery_price(mut self, price: f64) -> Self {
167 self.estimated_delivery_price = Some(price);
168 self
169 }
170
171 pub fn spread(&self) -> Option<f64> {
173 match (self.bid_price, self.ask_price) {
174 (Some(bid), Some(ask)) => Some(ask - bid),
175 _ => None,
176 }
177 }
178
179 pub fn spread_percentage(&self) -> Option<f64> {
181 match (self.spread(), self.mid_price) {
182 (Some(spread), Some(mid)) if mid > 0.0 => Some((spread / mid) * 100.0),
183 _ => None,
184 }
185 }
186
187 pub fn is_perpetual(&self) -> bool {
189 self.instrument_name.contains("PERPETUAL")
190 }
191
192 pub fn is_option(&self) -> bool {
194 !self.is_perpetual()
196 && (self.instrument_name.ends_with("-C") || self.instrument_name.ends_with("-P"))
197 }
198
199 pub fn is_future(&self) -> bool {
201 !self.is_perpetual() && !self.is_option()
202 }
203
204 pub fn price_change_absolute(&self) -> Option<f64> {
206 self.price_change.map(|change| {
207 if let Some(last) = self.last {
208 last * (change / 100.0)
209 } else {
210 self.mark_price * (change / 100.0)
211 }
212 })
213 }
214}
215
216impl_json_display!(BookSummary);
217impl_json_debug_pretty!(BookSummary);
218
219#[derive(Clone, PartialEq, Serialize, Deserialize)]
221pub struct BookSummaries {
222 pub summaries: Vec<BookSummary>,
224}
225
226impl BookSummaries {
227 pub fn new() -> Self {
229 Self {
230 summaries: Vec::new(),
231 }
232 }
233
234 pub fn add(&mut self, summary: BookSummary) {
236 self.summaries.push(summary);
237 }
238
239 pub fn by_currency(&self, currency: String) -> Vec<&BookSummary> {
241 self.summaries
242 .iter()
243 .filter(|s| s.base_currency == currency)
244 .collect()
245 }
246
247 pub fn perpetuals(&self) -> Vec<&BookSummary> {
249 self.summaries.iter().filter(|s| s.is_perpetual()).collect()
250 }
251
252 pub fn options(&self) -> Vec<&BookSummary> {
254 self.summaries.iter().filter(|s| s.is_option()).collect()
255 }
256
257 pub fn futures(&self) -> Vec<&BookSummary> {
259 self.summaries.iter().filter(|s| s.is_future()).collect()
260 }
261
262 pub fn sort_by_volume(&mut self) {
264 self.summaries
265 .sort_by(|a, b| b.volume_usd.partial_cmp(&a.volume_usd).unwrap());
266 }
267
268 pub fn sort_by_open_interest(&mut self) {
270 self.summaries
271 .sort_by(|a, b| b.open_interest.partial_cmp(&a.open_interest).unwrap());
272 }
273}
274
275impl Default for BookSummaries {
276 fn default() -> Self {
277 Self::new()
278 }
279}
280
281impl_json_display!(BookSummaries);
282impl_json_debug_pretty!(BookSummaries);
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn test_book_summary_creation() {
290 let summary = BookSummary::new(
291 "BTC-PERPETUAL".to_string(),
292 "BTC".to_string(),
293 "USD".to_string(),
294 45000.0,
295 1640995200000,
296 );
297
298 assert_eq!(summary.instrument_name, "BTC-PERPETUAL");
299 assert_eq!(summary.base_currency, "BTC".to_string());
300 assert_eq!(summary.mark_price, 45000.0);
301 assert!(summary.is_perpetual());
302 }
303
304 #[test]
305 fn test_book_summary_builder() {
306 let summary = BookSummary::new(
307 "BTC-25MAR23-50000-C".to_string(),
308 "BTC".to_string(),
309 "USD".to_string(),
310 2500.0,
311 1640995200000,
312 )
313 .with_volume(100.0, 4500000.0)
314 .with_prices(
315 Some(2480.0),
316 Some(2520.0),
317 Some(2500.0),
318 Some(2600.0),
319 Some(2400.0),
320 )
321 .with_iv(85.5);
322
323 assert_eq!(summary.volume, 100.0);
324 assert_eq!(summary.volume_usd, 4500000.0);
325 assert_eq!(summary.bid_price, Some(2480.0));
326 assert_eq!(summary.ask_price, Some(2520.0));
327 assert_eq!(summary.mid_price, Some(2500.0));
328 assert_eq!(summary.mark_iv, Some(85.5));
329 assert!(summary.is_option());
330 }
331
332 #[test]
333 fn test_spread_calculation() {
334 let summary = BookSummary::new(
335 "BTC-PERPETUAL".to_string(),
336 "BTC".to_string(),
337 "USD".to_string(),
338 45000.0,
339 1640995200000,
340 )
341 .with_prices(Some(44950.0), Some(45050.0), None, None, None);
342
343 assert_eq!(summary.spread(), Some(100.0));
344 assert_eq!(summary.mid_price, Some(45000.0));
345
346 let spread_pct = summary.spread_percentage().unwrap();
347 assert!((spread_pct - 0.2222).abs() < 0.001); }
349
350 #[test]
351 fn test_instrument_type_detection() {
352 let perpetual = BookSummary::new(
353 "BTC-PERPETUAL".to_string(),
354 "BTC".to_string(),
355 "USD".to_string(),
356 45000.0,
357 0,
358 );
359 assert!(perpetual.is_perpetual());
360 assert!(!perpetual.is_option());
361 assert!(!perpetual.is_future());
362
363 let option = BookSummary::new(
364 "BTC-25MAR23-50000-C".to_string(),
365 "BTC".to_string(),
366 "USD".to_string(),
367 2500.0,
368 0,
369 );
370 assert!(!option.is_perpetual());
371 assert!(option.is_option());
372 assert!(!option.is_future());
373
374 let future = BookSummary::new(
375 "BTC-25MAR23".to_string(),
376 "BTC".to_string(),
377 "USD".to_string(),
378 45000.0,
379 0,
380 );
381 assert!(!future.is_perpetual());
382 assert!(!future.is_option());
383 assert!(future.is_future());
384 }
385
386 #[test]
387 fn test_book_summaries_collection() {
388 let mut summaries = BookSummaries::new();
389
390 summaries.add(
391 BookSummary::new(
392 "BTC-PERPETUAL".to_string(),
393 "BTC".to_string(),
394 "USD".to_string(),
395 45000.0,
396 0,
397 )
398 .with_volume(1000.0, 45000000.0),
399 );
400
401 summaries.add(
402 BookSummary::new(
403 "ETH-PERPETUAL".to_string(),
404 "ETH".to_string(),
405 "USD".to_string(),
406 3000.0,
407 0,
408 )
409 .with_volume(500.0, 1500000.0),
410 );
411
412 assert_eq!(summaries.summaries.len(), 2);
413 assert_eq!(summaries.by_currency("BTC".to_string()).len(), 1);
414 assert_eq!(summaries.perpetuals().len(), 2);
415
416 summaries.sort_by_volume();
417 assert_eq!(summaries.summaries[0].base_currency, "BTC".to_string());
418 }
419
420 #[test]
421 fn test_serde() {
422 let summary = BookSummary::new(
423 "BTC-PERPETUAL".to_string(),
424 "BTC".to_string(),
425 "USD".to_string(),
426 45000.0,
427 1640995200000,
428 )
429 .with_funding(0.0001, 0.0008);
430
431 let json = serde_json::to_string(&summary).unwrap();
432 let deserialized: BookSummary = serde_json::from_str(&json).unwrap();
433 assert_eq!(summary, deserialized);
434 }
435}