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#[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)); 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)); 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); }
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); }
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); }
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}