Skip to main content

deribit_base/model/
block_trade.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 6/3/26
5******************************************************************************/
6
7//! Block trade data structures and types
8//!
9//! This module contains types for Deribit block trade operations,
10//! supporting bilateral/OTC trading between counterparties.
11//!
12//! Block trades allow large trades to be executed off the order book
13//! between two parties who have agreed on the terms.
14
15use crate::model::order::OrderSide;
16use pretty_simple_display::{DebugPretty, DisplaySimple};
17use serde::{Deserialize, Serialize};
18
19/// Role in a block trade
20///
21/// Indicates whether the party is the maker or taker in the block trade.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
23#[serde(rename_all = "lowercase")]
24pub enum BlockTradeRole {
25    /// Maker role - the party initiating the trade
26    #[default]
27    Maker,
28    /// Taker role - the party accepting the trade
29    Taker,
30}
31
32impl BlockTradeRole {
33    /// Get the string representation for API requests
34    #[must_use]
35    pub fn as_str(&self) -> &'static str {
36        match self {
37            Self::Maker => "maker",
38            Self::Taker => "taker",
39        }
40    }
41}
42
43impl std::fmt::Display for BlockTradeRole {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        write!(f, "{}", self.as_str())
46    }
47}
48
49/// Single trade leg for block trade request
50///
51/// Represents one instrument in a block trade, specifying the
52/// instrument, price, amount, and direction.
53#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
54pub struct BlockTradeLeg {
55    /// Instrument name (e.g., "BTC-PERPETUAL")
56    pub instrument_name: String,
57    /// Price for this leg
58    pub price: f64,
59    /// Trade amount (USD for perpetuals/inverse futures, base currency for options/linear)
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub amount: Option<f64>,
62    /// Direction from the maker's perspective
63    pub direction: OrderSide,
64}
65
66impl BlockTradeLeg {
67    /// Create a new block trade leg
68    #[must_use]
69    pub fn new(instrument_name: String, price: f64, amount: f64, direction: OrderSide) -> Self {
70        Self {
71            instrument_name,
72            price,
73            amount: Some(amount),
74            direction,
75        }
76    }
77}
78
79/// Block trade verification request
80///
81/// Used to verify and generate a signature for a block trade
82/// via `/private/verify_block_trade`.
83#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
84pub struct VerifyBlockTradeRequest {
85    /// Timestamp shared with counterparty (milliseconds since Unix epoch)
86    pub timestamp: i64,
87    /// Nonce shared with counterparty
88    pub nonce: String,
89    /// Role in the block trade (maker/taker)
90    pub role: BlockTradeRole,
91    /// List of trade legs
92    pub trades: Vec<BlockTradeLeg>,
93}
94
95impl VerifyBlockTradeRequest {
96    /// Create a new verification request
97    #[must_use]
98    pub fn new(
99        timestamp: i64,
100        nonce: String,
101        role: BlockTradeRole,
102        trades: Vec<BlockTradeLeg>,
103    ) -> Self {
104        Self {
105            timestamp,
106            nonce,
107            role,
108            trades,
109        }
110    }
111}
112
113/// Block trade signature response
114///
115/// Contains the signature generated by `/private/verify_block_trade`.
116/// The signature is valid for 5 minutes around the given timestamp.
117#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Eq, Serialize, Deserialize)]
118pub struct BlockTradeSignature {
119    /// Signature string for the block trade
120    pub signature: String,
121}
122
123impl BlockTradeSignature {
124    /// Create a new signature response
125    #[must_use]
126    pub fn new(signature: String) -> Self {
127        Self { signature }
128    }
129}
130
131/// Block trade execution request
132///
133/// Used to execute a block trade via `/private/execute_block_trade`.
134/// The request must match exactly what was used in `verify_block_trade`,
135/// with the counterparty's signature.
136#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
137pub struct ExecuteBlockTradeRequest {
138    /// Timestamp shared with counterparty (milliseconds since Unix epoch)
139    pub timestamp: i64,
140    /// Nonce shared with counterparty
141    pub nonce: String,
142    /// Role in the block trade (maker/taker)
143    pub role: BlockTradeRole,
144    /// List of trade legs
145    pub trades: Vec<BlockTradeLeg>,
146    /// Signature from counterparty's verify_block_trade call
147    pub counterparty_signature: String,
148}
149
150impl ExecuteBlockTradeRequest {
151    /// Create a new execution request
152    #[must_use]
153    pub fn new(
154        timestamp: i64,
155        nonce: String,
156        role: BlockTradeRole,
157        trades: Vec<BlockTradeLeg>,
158        counterparty_signature: String,
159    ) -> Self {
160        Self {
161            timestamp,
162            nonce,
163            role,
164            trades,
165            counterparty_signature,
166        }
167    }
168
169    /// Create from a verification request and counterparty signature
170    #[must_use]
171    pub fn from_verify_request(
172        verify_request: VerifyBlockTradeRequest,
173        counterparty_signature: String,
174    ) -> Self {
175        Self {
176            timestamp: verify_request.timestamp,
177            nonce: verify_request.nonce,
178            role: verify_request.role,
179            trades: verify_request.trades,
180            counterparty_signature,
181        }
182    }
183}
184
185/// Individual trade execution within a block trade
186///
187/// Contains details of a single executed trade leg within a block trade.
188#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
189pub struct BlockTradeExecution {
190    /// Unique trade identifier
191    pub trade_id: String,
192    /// Trade sequence number
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub trade_seq: Option<i64>,
195    /// Instrument name
196    pub instrument_name: String,
197    /// Trade direction (buy/sell)
198    pub direction: String,
199    /// Trade amount
200    pub amount: f64,
201    /// Execution price
202    pub price: f64,
203    /// Fee amount
204    pub fee: f64,
205    /// Fee currency (e.g., "BTC", "ETH")
206    pub fee_currency: String,
207    /// Order ID
208    pub order_id: String,
209    /// Order type (e.g., "limit")
210    pub order_type: String,
211    /// Liquidity indicator ("M" for maker, "T" for taker)
212    pub liquidity: String,
213    /// Index price at execution
214    pub index_price: f64,
215    /// Mark price at execution
216    pub mark_price: f64,
217    /// Block trade ID this execution belongs to
218    pub block_trade_id: String,
219    /// Execution timestamp in milliseconds
220    pub timestamp: i64,
221    /// Trade state (e.g., "filled")
222    pub state: String,
223    /// Tick direction (0=Plus, 1=Zero-Plus, 2=Minus, 3=Zero-Minus)
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub tick_direction: Option<i32>,
226    /// Whether this was an API order
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub api: Option<bool>,
229    /// Post-only flag
230    #[serde(skip_serializing_if = "Option::is_none")]
231    pub post_only: Option<bool>,
232    /// Reduce-only flag
233    #[serde(skip_serializing_if = "Option::is_none")]
234    pub reduce_only: Option<bool>,
235    /// Implied volatility (options only)
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub iv: Option<f64>,
238    /// Underlying price (options only)
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub underlying_price: Option<f64>,
241    /// Trade label
242    #[serde(skip_serializing_if = "Option::is_none")]
243    pub label: Option<String>,
244}
245
246/// Executed block trade
247///
248/// Contains the full details of an executed block trade,
249/// returned by `/private/execute_block_trade` or `/private/get_block_trade`.
250#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
251pub struct BlockTrade {
252    /// Block trade ID
253    pub id: String,
254    /// Execution timestamp in milliseconds
255    pub timestamp: i64,
256    /// List of executed trades in this block
257    pub trades: Vec<BlockTradeExecution>,
258    /// Application name that executed the block trade (optional)
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub app_name: Option<String>,
261    /// Broker code (for broker trades)
262    #[serde(skip_serializing_if = "Option::is_none")]
263    pub broker_code: Option<String>,
264    /// Broker name (for broker trades)
265    #[serde(skip_serializing_if = "Option::is_none")]
266    pub broker_name: Option<String>,
267}
268
269impl BlockTrade {
270    /// Get the total number of trades in this block
271    #[must_use]
272    pub fn trade_count(&self) -> usize {
273        self.trades.len()
274    }
275
276    /// Check if this is a broker-facilitated block trade
277    #[must_use]
278    pub fn is_broker_trade(&self) -> bool {
279        self.broker_code.is_some()
280    }
281
282    /// Get all unique instruments in this block trade
283    #[must_use]
284    pub fn instruments(&self) -> Vec<&str> {
285        let mut instruments: Vec<&str> = self
286            .trades
287            .iter()
288            .map(|t| t.instrument_name.as_str())
289            .collect();
290        instruments.sort();
291        instruments.dedup();
292        instruments
293    }
294
295    /// Calculate total fees across all trades
296    #[must_use]
297    pub fn total_fees(&self) -> f64 {
298        self.trades.iter().map(|t| t.fee).sum()
299    }
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305
306    #[test]
307    fn test_block_trade_role_default() {
308        let role = BlockTradeRole::default();
309        assert_eq!(role, BlockTradeRole::Maker);
310    }
311
312    #[test]
313    fn test_block_trade_role_as_str() {
314        assert_eq!(BlockTradeRole::Maker.as_str(), "maker");
315        assert_eq!(BlockTradeRole::Taker.as_str(), "taker");
316    }
317
318    #[test]
319    fn test_block_trade_role_display() {
320        assert_eq!(format!("{}", BlockTradeRole::Maker), "maker");
321        assert_eq!(format!("{}", BlockTradeRole::Taker), "taker");
322    }
323
324    #[test]
325    fn test_block_trade_role_serialization() {
326        let maker = BlockTradeRole::Maker;
327        let json = serde_json::to_string(&maker).unwrap();
328        assert_eq!(json, "\"maker\"");
329
330        let deserialized: BlockTradeRole = serde_json::from_str(&json).unwrap();
331        assert_eq!(deserialized, BlockTradeRole::Maker);
332    }
333
334    #[test]
335    fn test_block_trade_leg_new() {
336        let leg = BlockTradeLeg::new(
337            "BTC-PERPETUAL".to_string(),
338            50000.0,
339            10000.0,
340            OrderSide::Buy,
341        );
342        assert_eq!(leg.instrument_name, "BTC-PERPETUAL");
343        assert!((leg.price - 50000.0).abs() < f64::EPSILON);
344        assert_eq!(leg.amount, Some(10000.0));
345        assert_eq!(leg.direction, OrderSide::Buy);
346    }
347
348    #[test]
349    fn test_block_trade_leg_serialization() {
350        let leg = BlockTradeLeg::new(
351            "BTC-PERPETUAL".to_string(),
352            50000.0,
353            10000.0,
354            OrderSide::Buy,
355        );
356        let json = serde_json::to_string(&leg).unwrap();
357        let deserialized: BlockTradeLeg = serde_json::from_str(&json).unwrap();
358        assert_eq!(leg, deserialized);
359    }
360
361    #[test]
362    fn test_verify_block_trade_request_new() {
363        let trades = vec![BlockTradeLeg::new(
364            "BTC-PERPETUAL".to_string(),
365            50000.0,
366            10000.0,
367            OrderSide::Buy,
368        )];
369        let request = VerifyBlockTradeRequest::new(
370            1640995200000,
371            "test_nonce".to_string(),
372            BlockTradeRole::Maker,
373            trades,
374        );
375        assert_eq!(request.timestamp, 1640995200000);
376        assert_eq!(request.nonce, "test_nonce");
377        assert_eq!(request.role, BlockTradeRole::Maker);
378        assert_eq!(request.trades.len(), 1);
379    }
380
381    #[test]
382    fn test_block_trade_signature_new() {
383        let sig = BlockTradeSignature::new("test_signature_123".to_string());
384        assert_eq!(sig.signature, "test_signature_123");
385    }
386
387    #[test]
388    fn test_block_trade_signature_serialization() {
389        let sig = BlockTradeSignature::new("test_signature_123".to_string());
390        let json = serde_json::to_string(&sig).unwrap();
391        let deserialized: BlockTradeSignature = serde_json::from_str(&json).unwrap();
392        assert_eq!(sig, deserialized);
393    }
394
395    #[test]
396    fn test_execute_block_trade_request_new() {
397        let trades = vec![BlockTradeLeg::new(
398            "BTC-PERPETUAL".to_string(),
399            50000.0,
400            10000.0,
401            OrderSide::Buy,
402        )];
403        let request = ExecuteBlockTradeRequest::new(
404            1640995200000,
405            "test_nonce".to_string(),
406            BlockTradeRole::Maker,
407            trades,
408            "counterparty_sig".to_string(),
409        );
410        assert_eq!(request.timestamp, 1640995200000);
411        assert_eq!(request.counterparty_signature, "counterparty_sig");
412    }
413
414    #[test]
415    fn test_execute_block_trade_request_from_verify() {
416        let trades = vec![BlockTradeLeg::new(
417            "BTC-PERPETUAL".to_string(),
418            50000.0,
419            10000.0,
420            OrderSide::Buy,
421        )];
422        let verify_request = VerifyBlockTradeRequest::new(
423            1640995200000,
424            "test_nonce".to_string(),
425            BlockTradeRole::Maker,
426            trades,
427        );
428        let exec_request = ExecuteBlockTradeRequest::from_verify_request(
429            verify_request.clone(),
430            "counterparty_sig".to_string(),
431        );
432        assert_eq!(exec_request.timestamp, verify_request.timestamp);
433        assert_eq!(exec_request.nonce, verify_request.nonce);
434        assert_eq!(exec_request.role, verify_request.role);
435        assert_eq!(exec_request.counterparty_signature, "counterparty_sig");
436    }
437
438    fn create_test_block_trade_execution() -> BlockTradeExecution {
439        BlockTradeExecution {
440            trade_id: "48079573".to_string(),
441            trade_seq: Some(30289730),
442            instrument_name: "BTC-PERPETUAL".to_string(),
443            direction: "sell".to_string(),
444            amount: 200000.0,
445            price: 8900.0,
446            fee: -0.00561798,
447            fee_currency: "BTC".to_string(),
448            order_id: "4009043192".to_string(),
449            order_type: "limit".to_string(),
450            liquidity: "M".to_string(),
451            index_price: 8900.45,
452            mark_price: 8895.19,
453            block_trade_id: "6165".to_string(),
454            timestamp: 1590485535978,
455            state: "filled".to_string(),
456            tick_direction: Some(0),
457            api: None,
458            post_only: Some(false),
459            reduce_only: Some(false),
460            iv: None,
461            underlying_price: None,
462            label: None,
463        }
464    }
465
466    #[test]
467    fn test_block_trade_execution_serialization() {
468        let exec = create_test_block_trade_execution();
469        let json = serde_json::to_string(&exec).unwrap();
470        let deserialized: BlockTradeExecution = serde_json::from_str(&json).unwrap();
471        assert_eq!(exec.trade_id, deserialized.trade_id);
472        assert_eq!(exec.instrument_name, deserialized.instrument_name);
473    }
474
475    #[test]
476    fn test_block_trade_trade_count() {
477        let block_trade = BlockTrade {
478            id: "6165".to_string(),
479            timestamp: 1590485535980,
480            trades: vec![
481                create_test_block_trade_execution(),
482                create_test_block_trade_execution(),
483            ],
484            app_name: None,
485            broker_code: None,
486            broker_name: None,
487        };
488        assert_eq!(block_trade.trade_count(), 2);
489    }
490
491    #[test]
492    fn test_block_trade_is_broker_trade() {
493        let regular_trade = BlockTrade {
494            id: "6165".to_string(),
495            timestamp: 1590485535980,
496            trades: vec![],
497            app_name: None,
498            broker_code: None,
499            broker_name: None,
500        };
501        assert!(!regular_trade.is_broker_trade());
502
503        let broker_trade = BlockTrade {
504            id: "6165".to_string(),
505            timestamp: 1590485535980,
506            trades: vec![],
507            app_name: None,
508            broker_code: Some("BROKER123".to_string()),
509            broker_name: Some("Test Broker".to_string()),
510        };
511        assert!(broker_trade.is_broker_trade());
512    }
513
514    #[test]
515    fn test_block_trade_instruments() {
516        let mut exec1 = create_test_block_trade_execution();
517        exec1.instrument_name = "BTC-PERPETUAL".to_string();
518
519        let mut exec2 = create_test_block_trade_execution();
520        exec2.instrument_name = "BTC-28MAY20-9000-C".to_string();
521
522        let block_trade = BlockTrade {
523            id: "6165".to_string(),
524            timestamp: 1590485535980,
525            trades: vec![exec1, exec2],
526            app_name: None,
527            broker_code: None,
528            broker_name: None,
529        };
530
531        let instruments = block_trade.instruments();
532        assert_eq!(instruments.len(), 2);
533        assert!(instruments.contains(&"BTC-PERPETUAL"));
534        assert!(instruments.contains(&"BTC-28MAY20-9000-C"));
535    }
536
537    #[test]
538    fn test_block_trade_total_fees() {
539        let mut exec1 = create_test_block_trade_execution();
540        exec1.fee = 0.001;
541
542        let mut exec2 = create_test_block_trade_execution();
543        exec2.fee = 0.002;
544
545        let block_trade = BlockTrade {
546            id: "6165".to_string(),
547            timestamp: 1590485535980,
548            trades: vec![exec1, exec2],
549            app_name: None,
550            broker_code: None,
551            broker_name: None,
552        };
553
554        assert!((block_trade.total_fees() - 0.003).abs() < f64::EPSILON);
555    }
556
557    #[test]
558    fn test_block_trade_serialization() {
559        let block_trade = BlockTrade {
560            id: "6165".to_string(),
561            timestamp: 1590485535980,
562            trades: vec![create_test_block_trade_execution()],
563            app_name: Some("TestApp".to_string()),
564            broker_code: None,
565            broker_name: None,
566        };
567        let json = serde_json::to_string(&block_trade).unwrap();
568        let deserialized: BlockTrade = serde_json::from_str(&json).unwrap();
569        assert_eq!(block_trade.id, deserialized.id);
570        assert_eq!(block_trade.app_name, deserialized.app_name);
571    }
572}