deribit_base/model/
trade.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, order::OrderSide};
7use pretty_simple_display::{DebugPretty, DisplaySimple};
8use serde::{Deserialize, Serialize};
9
10/// Liquidity type enumeration
11#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Eq, Serialize, Deserialize)]
12pub enum Liquidity {
13    /// Maker (provided liquidity)
14    #[serde(rename = "M")]
15    Maker,
16    /// Taker (consumed liquidity)
17    #[serde(rename = "T")]
18    Taker,
19    /// Mixed (both maker and taker in same trade)
20    #[serde(rename = "MT")]
21    Mixed,
22}
23
24/// Trade execution information
25#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
26pub struct Trade {
27    /// Unique trade identifier
28    pub trade_id: String,
29    /// Instrument name
30    pub instrument_name: String,
31    /// Order ID that generated this trade
32    pub order_id: String,
33    /// Trade direction (buy/sell)
34    pub direction: OrderSide,
35    /// Trade amount
36    pub amount: f64,
37    /// Execution price
38    pub price: f64,
39    /// Trade timestamp
40    pub timestamp: i64,
41    /// Fee amount
42    pub fee: f64,
43    /// Fee currency
44    pub fee_currency: String,
45    /// Liquidity type (maker/taker)
46    pub liquidity: Liquidity,
47    /// Mark price at time of trade
48    pub mark_price: f64,
49    /// Index price at time of trade
50    pub index_price: f64,
51    /// Instrument kind
52    pub instrument_kind: Option<InstrumentKind>,
53    /// Trade sequence number
54    pub trade_seq: Option<u64>,
55    /// User role in the trade
56    pub user_role: Option<String>,
57    /// Whether this is a block trade
58    pub block_trade: Option<bool>,
59    /// Underlying price (for options)
60    pub underlying_price: Option<f64>,
61    /// Implied volatility (for options)
62    pub iv: Option<f64>,
63    /// Label associated with the order
64    pub label: Option<String>,
65    /// Profit and loss from this trade
66    pub profit_loss: Option<f64>,
67    /// Tick direction
68    pub tick_direction: Option<i32>,
69    /// Whether this trade was self-traded
70    pub self_trade: Option<bool>,
71}
72
73impl Trade {
74    /// Calculate the notional value of the trade
75    pub fn notional_value(&self) -> f64 {
76        self.amount * self.price
77    }
78
79    /// Check if this was a maker trade
80    pub fn is_maker(&self) -> bool {
81        matches!(self.liquidity, Liquidity::Maker | Liquidity::Mixed)
82    }
83
84    /// Check if this was a taker trade
85    pub fn is_taker(&self) -> bool {
86        matches!(self.liquidity, Liquidity::Taker | Liquidity::Mixed)
87    }
88
89    /// Check if this is a buy trade
90    pub fn is_buy(&self) -> bool {
91        self.direction == OrderSide::Buy
92    }
93
94    /// Check if this is a sell trade
95    pub fn is_sell(&self) -> bool {
96        self.direction == OrderSide::Sell
97    }
98
99    /// Get fee as percentage of notional
100    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/// Trade statistics
110#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
111pub struct TradeStats {
112    /// Total number of trades
113    pub count: u64,
114    /// Total volume
115    pub volume: f64,
116    /// Total fees paid
117    pub total_fees: f64,
118    /// Average price
119    pub avg_price: f64,
120    /// Profit and loss
121    pub pnl: f64,
122    /// Number of winning trades
123    pub winning_trades: u64,
124    /// Number of losing trades
125    pub losing_trades: u64,
126}
127
128impl TradeStats {
129    /// Create empty trade statistics
130    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    /// Calculate win rate as percentage
143    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/// Trade execution
159#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
160pub struct TradeExecution {
161    /// Trade amount
162    pub amount: f64,
163    /// Trade direction (buy/sell)
164    pub direction: String,
165    /// Trading fee paid
166    pub fee: f64,
167    /// Currency of the trading fee
168    pub fee_currency: String,
169    /// Index price at execution time
170    pub index_price: f64,
171    /// Name of the traded instrument
172    pub instrument_name: String,
173    /// Implied volatility (for options)
174    pub iv: Option<f64>,
175    /// User-defined label for the trade
176    pub label: String,
177    /// Liquidity type (maker/taker)
178    pub liquidity: String,
179    /// Mark price at execution time
180    pub mark_price: f64,
181    /// Matching engine identifier
182    pub matching_id: Option<String>,
183    /// Order ID that generated this trade
184    pub order_id: String,
185    /// Type of the order that generated this trade
186    pub order_type: String,
187    /// Original order type before modifications
188    pub original_order_type: Option<String>,
189    /// Execution price
190    pub price: f64,
191    /// Whether this was a self trade
192    pub self_trade: bool,
193    /// Current state of the trade
194    pub state: String,
195    /// Price tick direction (1=up, -1=down, 0=no change)
196    pub tick_direction: i32,
197    /// Execution timestamp
198    pub timestamp: u64,
199    /// Unique trade identifier
200    pub trade_id: String,
201    /// Trade sequence number
202    pub trade_seq: u64,
203    /// Underlying asset price (for derivatives)
204    pub underlying_price: Option<f64>,
205}
206
207/// User trade information
208#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
209pub struct UserTrade {
210    /// Trade amount
211    pub amount: f64,
212    /// Trade direction (buy/sell)
213    pub direction: String,
214    /// Trading fee paid
215    pub fee: f64,
216    /// Currency of the trading fee
217    pub fee_currency: String,
218    /// Index price at execution time
219    pub index_price: f64,
220    /// Name of the traded instrument
221    pub instrument_name: String,
222    /// Implied volatility (for options)
223    pub iv: Option<f64>,
224    /// User-defined label for the trade
225    pub label: String,
226    /// Liquidity type (maker/taker)
227    pub liquidity: String,
228    /// Mark price at execution time
229    pub mark_price: f64,
230    /// Matching engine identifier
231    pub matching_id: Option<String>,
232    /// Order ID that generated this trade
233    pub order_id: String,
234    /// Type of the order that generated this trade
235    pub order_type: String,
236    /// Original order type before modifications
237    pub original_order_type: Option<String>,
238    /// Execution price
239    pub price: f64,
240    /// Whether this was a self trade
241    pub self_trade: bool,
242    /// Current state of the trade
243    pub state: String,
244    /// Price tick direction (1=up, -1=down, 0=no change)
245    pub tick_direction: i32,
246    /// Execution timestamp
247    pub timestamp: u64,
248    /// Unique trade identifier
249    pub trade_id: String,
250    /// Trade sequence number
251    pub trade_seq: u64,
252    /// Underlying asset price (for derivatives)
253    pub underlying_price: Option<f64>,
254}
255
256/// Last trade
257#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
258pub struct LastTrade {
259    /// Trade amount
260    pub amount: f64,
261    /// Trade direction (buy/sell)
262    pub direction: String,
263    /// Index price at execution time
264    pub index_price: f64,
265    /// Name of the traded instrument
266    pub instrument_name: String,
267    /// Implied volatility (for options)
268    pub iv: Option<f64>,
269    /// Liquidity information
270    pub liquid: Option<String>,
271    /// Execution price
272    pub price: f64,
273    /// Price tick direction (1=up, -1=down, 0=no change)
274    pub tick_direction: i32,
275    /// Execution timestamp
276    pub timestamp: u64,
277    /// Unique trade identifier
278    pub trade_id: String,
279    /// Trade sequence number
280    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); // 25 / 50000 * 100
503
504        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}