1use crate::model::{instrument::InstrumentKind, order::OrderSide};
7use pretty_simple_display::{DebugPretty, DisplaySimple};
8use serde::{Deserialize, Serialize};
9
10#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Eq, Serialize, Deserialize)]
12pub enum Liquidity {
13 #[serde(rename = "M")]
15 Maker,
16 #[serde(rename = "T")]
18 Taker,
19 #[serde(rename = "MT")]
21 Mixed,
22}
23
24#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
26pub struct Trade {
27 pub trade_id: String,
29 pub instrument_name: String,
31 pub order_id: String,
33 pub direction: OrderSide,
35 pub amount: f64,
37 pub price: f64,
39 pub timestamp: i64,
41 pub fee: f64,
43 pub fee_currency: String,
45 pub liquidity: Liquidity,
47 pub mark_price: f64,
49 pub index_price: f64,
51 pub instrument_kind: Option<InstrumentKind>,
53 pub trade_seq: Option<u64>,
55 pub user_role: Option<String>,
57 pub block_trade: Option<bool>,
59 pub underlying_price: Option<f64>,
61 pub iv: Option<f64>,
63 pub label: Option<String>,
65 pub profit_loss: Option<f64>,
67 pub tick_direction: Option<i32>,
69 pub self_trade: Option<bool>,
71}
72
73impl Trade {
74 pub fn notional_value(&self) -> f64 {
76 self.amount * self.price
77 }
78
79 pub fn is_maker(&self) -> bool {
81 matches!(self.liquidity, Liquidity::Maker | Liquidity::Mixed)
82 }
83
84 pub fn is_taker(&self) -> bool {
86 matches!(self.liquidity, Liquidity::Taker | Liquidity::Mixed)
87 }
88
89 pub fn is_buy(&self) -> bool {
91 self.direction == OrderSide::Buy
92 }
93
94 pub fn is_sell(&self) -> bool {
96 self.direction == OrderSide::Sell
97 }
98
99 pub fn fee_percentage(&self) -> f64 {
101 if self.notional_value() != 0.0 {
102 (self.fee / self.notional_value()) * 100.0
103 } else {
104 0.0
105 }
106 }
107}
108
109#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
111pub struct TradeStats {
112 pub count: u64,
114 pub volume: f64,
116 pub total_fees: f64,
118 pub avg_price: f64,
120 pub pnl: f64,
122 pub winning_trades: u64,
124 pub losing_trades: u64,
126}
127
128impl TradeStats {
129 pub fn new() -> Self {
131 Self {
132 count: 0,
133 volume: 0.0,
134 total_fees: 0.0,
135 avg_price: 0.0,
136 pnl: 0.0,
137 winning_trades: 0,
138 losing_trades: 0,
139 }
140 }
141
142 pub fn win_rate(&self) -> f64 {
144 if self.count > 0 {
145 (self.winning_trades as f64 / self.count as f64) * 100.0
146 } else {
147 0.0
148 }
149 }
150}
151
152impl Default for TradeStats {
153 fn default() -> Self {
154 Self::new()
155 }
156}
157
158#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
160pub struct TradeExecution {
161 pub amount: f64,
163 pub direction: String,
165 pub fee: f64,
167 pub fee_currency: String,
169 pub index_price: f64,
171 pub instrument_name: String,
173 pub iv: Option<f64>,
175 pub label: String,
177 pub liquidity: String,
179 pub mark_price: f64,
181 pub matching_id: Option<String>,
183 pub order_id: String,
185 pub order_type: String,
187 pub original_order_type: Option<String>,
189 pub price: f64,
191 pub self_trade: bool,
193 pub state: String,
195 pub tick_direction: i32,
197 pub timestamp: u64,
199 pub trade_id: String,
201 pub trade_seq: u64,
203 pub underlying_price: Option<f64>,
205}
206
207#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
209pub struct UserTrade {
210 pub amount: f64,
212 pub direction: String,
214 pub fee: f64,
216 pub fee_currency: String,
218 pub index_price: f64,
220 pub instrument_name: String,
222 pub iv: Option<f64>,
224 pub label: String,
226 pub liquidity: String,
228 pub mark_price: f64,
230 pub matching_id: Option<String>,
232 pub order_id: String,
234 pub order_type: String,
236 pub original_order_type: Option<String>,
238 pub price: f64,
240 pub self_trade: bool,
242 pub state: String,
244 pub tick_direction: i32,
246 pub timestamp: u64,
248 pub trade_id: String,
250 pub trade_seq: u64,
252 pub underlying_price: Option<f64>,
254}
255
256#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
258pub struct LastTrade {
259 pub amount: f64,
261 pub direction: String,
263 pub index_price: f64,
265 pub instrument_name: String,
267 pub iv: Option<f64>,
269 pub liquid: Option<String>,
271 pub price: f64,
273 pub tick_direction: i32,
275 pub timestamp: u64,
277 pub trade_id: String,
279 pub trade_seq: u64,
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286 use crate::model::instrument::InstrumentKind;
287 use crate::model::order::OrderSide;
288
289 #[test]
290 fn test_liquidity_variants() {
291 let maker = Liquidity::Maker;
292 let taker = Liquidity::Taker;
293 let mixed = Liquidity::Mixed;
294
295 assert_eq!(maker, Liquidity::Maker);
296 assert_eq!(taker, Liquidity::Taker);
297 assert_eq!(mixed, Liquidity::Mixed);
298 }
299
300 #[test]
301 fn test_liquidity_serialization() {
302 let maker = Liquidity::Maker;
303 let taker = Liquidity::Taker;
304 let mixed = Liquidity::Mixed;
305
306 let maker_json = serde_json::to_string(&maker).unwrap();
307 let taker_json = serde_json::to_string(&taker).unwrap();
308 let mixed_json = serde_json::to_string(&mixed).unwrap();
309
310 assert_eq!(maker_json, "\"M\"");
311 assert_eq!(taker_json, "\"T\"");
312 assert_eq!(mixed_json, "\"MT\"");
313
314 let maker_deserialized: Liquidity = serde_json::from_str(&maker_json).unwrap();
315 let taker_deserialized: Liquidity = serde_json::from_str(&taker_json).unwrap();
316 let mixed_deserialized: Liquidity = serde_json::from_str(&mixed_json).unwrap();
317
318 assert_eq!(maker_deserialized, Liquidity::Maker);
319 assert_eq!(taker_deserialized, Liquidity::Taker);
320 assert_eq!(mixed_deserialized, Liquidity::Mixed);
321 }
322
323 #[test]
324 fn test_trade_creation() {
325 let trade = Trade {
326 trade_id: "12345".to_string(),
327 instrument_name: "BTC-PERPETUAL".to_string(),
328 order_id: "order_123".to_string(),
329 direction: OrderSide::Buy,
330 amount: 1.0,
331 price: 50000.0,
332 timestamp: 1640995200000,
333 fee: 25.0,
334 fee_currency: "USD".to_string(),
335 liquidity: Liquidity::Maker,
336 mark_price: 50010.0,
337 index_price: 50005.0,
338 instrument_kind: Some(InstrumentKind::Future),
339 trade_seq: Some(12345),
340 user_role: Some("maker".to_string()),
341 block_trade: Some(false),
342 underlying_price: Some(50000.0),
343 iv: None,
344 label: Some("test_trade".to_string()),
345 profit_loss: Some(100.0),
346 tick_direction: Some(1),
347 self_trade: Some(false),
348 };
349
350 assert_eq!(trade.trade_id, "12345");
351 assert_eq!(trade.instrument_name, "BTC-PERPETUAL");
352 assert_eq!(trade.direction, OrderSide::Buy);
353 assert_eq!(trade.amount, 1.0);
354 assert_eq!(trade.price, 50000.0);
355 assert_eq!(trade.fee, 25.0);
356 assert_eq!(trade.liquidity, Liquidity::Maker);
357 }
358
359 #[test]
360 fn test_trade_notional_value() {
361 let trade = Trade {
362 trade_id: "12345".to_string(),
363 instrument_name: "BTC-PERPETUAL".to_string(),
364 order_id: "order_123".to_string(),
365 direction: OrderSide::Buy,
366 amount: 2.0,
367 price: 50000.0,
368 timestamp: 1640995200000,
369 fee: 50.0,
370 fee_currency: "USD".to_string(),
371 liquidity: Liquidity::Maker,
372 mark_price: 50010.0,
373 index_price: 50005.0,
374 instrument_kind: None,
375 trade_seq: None,
376 user_role: None,
377 block_trade: None,
378 underlying_price: None,
379 iv: None,
380 label: None,
381 profit_loss: None,
382 tick_direction: None,
383 self_trade: None,
384 };
385
386 assert_eq!(trade.notional_value(), 100000.0);
387 }
388
389 #[test]
390 fn test_trade_liquidity_checks() {
391 let maker_trade = Trade {
392 trade_id: "1".to_string(),
393 instrument_name: "BTC-PERPETUAL".to_string(),
394 order_id: "order_1".to_string(),
395 direction: OrderSide::Buy,
396 amount: 1.0,
397 price: 50000.0,
398 timestamp: 1640995200000,
399 fee: 25.0,
400 fee_currency: "USD".to_string(),
401 liquidity: Liquidity::Maker,
402 mark_price: 50000.0,
403 index_price: 50000.0,
404 instrument_kind: None,
405 trade_seq: None,
406 user_role: None,
407 block_trade: None,
408 underlying_price: None,
409 iv: None,
410 label: None,
411 profit_loss: None,
412 tick_direction: None,
413 self_trade: None,
414 };
415
416 let taker_trade = Trade {
417 liquidity: Liquidity::Taker,
418 ..maker_trade.clone()
419 };
420
421 let mixed_trade = Trade {
422 liquidity: Liquidity::Mixed,
423 ..maker_trade.clone()
424 };
425
426 assert!(maker_trade.is_maker());
427 assert!(!maker_trade.is_taker());
428
429 assert!(!taker_trade.is_maker());
430 assert!(taker_trade.is_taker());
431
432 assert!(mixed_trade.is_maker());
433 assert!(mixed_trade.is_taker());
434 }
435
436 #[test]
437 fn test_trade_direction_checks() {
438 let buy_trade = Trade {
439 trade_id: "1".to_string(),
440 instrument_name: "BTC-PERPETUAL".to_string(),
441 order_id: "order_1".to_string(),
442 direction: OrderSide::Buy,
443 amount: 1.0,
444 price: 50000.0,
445 timestamp: 1640995200000,
446 fee: 25.0,
447 fee_currency: "USD".to_string(),
448 liquidity: Liquidity::Maker,
449 mark_price: 50000.0,
450 index_price: 50000.0,
451 instrument_kind: None,
452 trade_seq: None,
453 user_role: None,
454 block_trade: None,
455 underlying_price: None,
456 iv: None,
457 label: None,
458 profit_loss: None,
459 tick_direction: None,
460 self_trade: None,
461 };
462
463 let sell_trade = Trade {
464 direction: OrderSide::Sell,
465 ..buy_trade.clone()
466 };
467
468 assert!(buy_trade.is_buy());
469 assert!(!buy_trade.is_sell());
470
471 assert!(!sell_trade.is_buy());
472 assert!(sell_trade.is_sell());
473 }
474
475 #[test]
476 fn test_trade_fee_percentage() {
477 let trade = Trade {
478 trade_id: "1".to_string(),
479 instrument_name: "BTC-PERPETUAL".to_string(),
480 order_id: "order_1".to_string(),
481 direction: OrderSide::Buy,
482 amount: 1.0,
483 price: 50000.0,
484 timestamp: 1640995200000,
485 fee: 25.0,
486 fee_currency: "USD".to_string(),
487 liquidity: Liquidity::Maker,
488 mark_price: 50000.0,
489 index_price: 50000.0,
490 instrument_kind: None,
491 trade_seq: None,
492 user_role: None,
493 block_trade: None,
494 underlying_price: None,
495 iv: None,
496 label: None,
497 profit_loss: None,
498 tick_direction: None,
499 self_trade: None,
500 };
501
502 assert_eq!(trade.fee_percentage(), 0.05); let zero_notional_trade = Trade {
505 amount: 0.0,
506 price: 0.0,
507 ..trade
508 };
509
510 assert_eq!(zero_notional_trade.fee_percentage(), 0.0);
511 }
512
513 #[test]
514 fn test_trade_stats_new() {
515 let stats = TradeStats::new();
516 assert_eq!(stats.count, 0);
517 assert_eq!(stats.volume, 0.0);
518 assert_eq!(stats.total_fees, 0.0);
519 assert_eq!(stats.avg_price, 0.0);
520 assert_eq!(stats.pnl, 0.0);
521 assert_eq!(stats.winning_trades, 0);
522 assert_eq!(stats.losing_trades, 0);
523 }
524
525 #[test]
526 fn test_trade_stats_default() {
527 let stats = TradeStats::default();
528 assert_eq!(stats.count, 0);
529 assert_eq!(stats.volume, 0.0);
530 assert_eq!(stats.total_fees, 0.0);
531 assert_eq!(stats.avg_price, 0.0);
532 assert_eq!(stats.pnl, 0.0);
533 assert_eq!(stats.winning_trades, 0);
534 assert_eq!(stats.losing_trades, 0);
535 }
536
537 #[test]
538 fn test_trade_stats_win_rate() {
539 let mut stats = TradeStats::new();
540 stats.count = 10;
541 stats.winning_trades = 7;
542 stats.losing_trades = 3;
543
544 assert_eq!(stats.win_rate(), 70.0);
545
546 let empty_stats = TradeStats::new();
547 assert_eq!(empty_stats.win_rate(), 0.0);
548 }
549
550 #[test]
551 fn test_trade_execution_creation() {
552 let execution = TradeExecution {
553 amount: 1.0,
554 direction: "buy".to_string(),
555 fee: 25.0,
556 fee_currency: "USD".to_string(),
557 index_price: 50005.0,
558 instrument_name: "BTC-PERPETUAL".to_string(),
559 iv: Some(0.5),
560 label: "test_label".to_string(),
561 liquidity: "M".to_string(),
562 mark_price: 50010.0,
563 matching_id: Some("match_123".to_string()),
564 order_id: "order_123".to_string(),
565 order_type: "limit".to_string(),
566 original_order_type: Some("limit".to_string()),
567 price: 50000.0,
568 self_trade: false,
569 state: "filled".to_string(),
570 tick_direction: 1,
571 timestamp: 1640995200000,
572 trade_id: "trade_123".to_string(),
573 trade_seq: 12345,
574 underlying_price: Some(50000.0),
575 };
576
577 assert_eq!(execution.amount, 1.0);
578 assert_eq!(execution.direction, "buy");
579 assert_eq!(execution.fee, 25.0);
580 assert_eq!(execution.instrument_name, "BTC-PERPETUAL");
581 assert_eq!(execution.price, 50000.0);
582 assert_eq!(execution.trade_id, "trade_123");
583 assert!(!execution.self_trade);
584 }
585
586 #[test]
587 fn test_user_trade_creation() {
588 let user_trade = UserTrade {
589 amount: 2.0,
590 direction: "sell".to_string(),
591 fee: 50.0,
592 fee_currency: "USD".to_string(),
593 index_price: 49995.0,
594 instrument_name: "ETH-PERPETUAL".to_string(),
595 iv: None,
596 label: "user_label".to_string(),
597 liquidity: "T".to_string(),
598 mark_price: 49990.0,
599 matching_id: None,
600 order_id: "user_order_456".to_string(),
601 order_type: "market".to_string(),
602 original_order_type: None,
603 price: 49985.0,
604 self_trade: true,
605 state: "filled".to_string(),
606 tick_direction: -1,
607 timestamp: 1640995300000,
608 trade_id: "user_trade_456".to_string(),
609 trade_seq: 12346,
610 underlying_price: None,
611 };
612
613 assert_eq!(user_trade.amount, 2.0);
614 assert_eq!(user_trade.direction, "sell");
615 assert_eq!(user_trade.fee, 50.0);
616 assert_eq!(user_trade.instrument_name, "ETH-PERPETUAL");
617 assert_eq!(user_trade.price, 49985.0);
618 assert_eq!(user_trade.trade_id, "user_trade_456");
619 assert!(user_trade.self_trade);
620 assert_eq!(user_trade.tick_direction, -1);
621 }
622
623 #[test]
624 fn test_last_trade_creation() {
625 let last_trade = LastTrade {
626 amount: 0.5,
627 direction: "buy".to_string(),
628 index_price: 50005.0,
629 instrument_name: "BTC-25DEC24-50000-C".to_string(),
630 iv: Some(0.75),
631 liquid: Some("liquid".to_string()),
632 price: 2500.0,
633 tick_direction: 0,
634 timestamp: 1640995400000,
635 trade_id: "last_trade_789".to_string(),
636 trade_seq: 12347,
637 };
638
639 assert_eq!(last_trade.amount, 0.5);
640 assert_eq!(last_trade.direction, "buy");
641 assert_eq!(last_trade.index_price, 50005.0);
642 assert_eq!(last_trade.instrument_name, "BTC-25DEC24-50000-C");
643 assert_eq!(last_trade.iv, Some(0.75));
644 assert_eq!(last_trade.price, 2500.0);
645 assert_eq!(last_trade.tick_direction, 0);
646 assert_eq!(last_trade.trade_id, "last_trade_789");
647 assert_eq!(last_trade.trade_seq, 12347);
648 }
649
650 #[test]
651 fn test_serialization_roundtrip() {
652 let trade = Trade {
653 trade_id: "test_trade".to_string(),
654 instrument_name: "BTC-PERPETUAL".to_string(),
655 order_id: "order_123".to_string(),
656 direction: OrderSide::Buy,
657 amount: 1.0,
658 price: 50000.0,
659 timestamp: 1640995200000,
660 fee: 25.0,
661 fee_currency: "USD".to_string(),
662 liquidity: Liquidity::Maker,
663 mark_price: 50010.0,
664 index_price: 50005.0,
665 instrument_kind: Some(InstrumentKind::Future),
666 trade_seq: Some(12345),
667 user_role: Some("maker".to_string()),
668 block_trade: Some(false),
669 underlying_price: Some(50000.0),
670 iv: None,
671 label: Some("test_label".to_string()),
672 profit_loss: Some(100.0),
673 tick_direction: Some(1),
674 self_trade: Some(false),
675 };
676
677 let json = serde_json::to_string(&trade).unwrap();
678 let deserialized: Trade = serde_json::from_str(&json).unwrap();
679
680 assert_eq!(trade.trade_id, deserialized.trade_id);
681 assert_eq!(trade.instrument_name, deserialized.instrument_name);
682 assert_eq!(trade.direction, deserialized.direction);
683 assert_eq!(trade.amount, deserialized.amount);
684 assert_eq!(trade.price, deserialized.price);
685 assert_eq!(trade.liquidity, deserialized.liquidity);
686 }
687
688 #[test]
689 fn test_debug_and_display_implementations() {
690 let liquidity = Liquidity::Maker;
691 let debug_str = format!("{:?}", liquidity);
692 let display_str = format!("{}", liquidity);
693
694 assert!(debug_str.contains("Maker") || debug_str.contains("M"));
695 assert!(display_str.contains("M"));
696
697 let stats = TradeStats::new();
698 let stats_debug = format!("{:?}", stats);
699 let stats_display = format!("{}", stats);
700
701 assert!(stats_debug.contains("count") || stats_debug.contains("0"));
702 assert!(stats_display.contains("0"));
703 }
704
705 #[test]
706 fn test_cloning() {
707 let trade = Trade {
708 trade_id: "clone_test".to_string(),
709 instrument_name: "BTC-PERPETUAL".to_string(),
710 order_id: "order_123".to_string(),
711 direction: OrderSide::Buy,
712 amount: 1.0,
713 price: 50000.0,
714 timestamp: 1640995200000,
715 fee: 25.0,
716 fee_currency: "USD".to_string(),
717 liquidity: Liquidity::Maker,
718 mark_price: 50010.0,
719 index_price: 50005.0,
720 instrument_kind: None,
721 trade_seq: None,
722 user_role: None,
723 block_trade: None,
724 underlying_price: None,
725 iv: None,
726 label: None,
727 profit_loss: None,
728 tick_direction: None,
729 self_trade: None,
730 };
731
732 let cloned_trade = trade.clone();
733 assert_eq!(trade.trade_id, cloned_trade.trade_id);
734 assert_eq!(trade.amount, cloned_trade.amount);
735 assert_eq!(trade.price, cloned_trade.price);
736 assert_eq!(trade.liquidity, cloned_trade.liquidity);
737
738 let liquidity = Liquidity::Taker;
739 let cloned_liquidity = liquidity.clone();
740 assert_eq!(liquidity, cloned_liquidity);
741 }
742}